Compare commits

...

670 commits

Author SHA1 Message Date
6fbb6547bc Merge remote-tracking branch 'upstream/develop' into seitime 2026-05-05 18:51:04 -06:00
Soham Kulkarni
820bb380e4
Merge pull request #39068 from frappe/pot_develop_2026-05-03 2026-05-05 20:31:42 +05:30
Aarol D'Souza
cbd6d59cf1
Merge pull request #39083 from AarDG10/fix-img-css
fix: fix image overflow by restricting width to 100% of column
2026-05-05 10:30:54 +05:30
Aarol D'Souza
4a0be898af
Merge pull request #39029 from kaulith/fix/safe-filters-notation
fix: preserve docnames matching scientific notation in get_safe_filters
2026-05-05 10:27:41 +05:30
AarDG10
668e78709f fix: fix image overflow by restricting width to 100% of column
Image was overflowing onto the right sidebar. This was due to the width not being bounded, added max-width to fix this.
2026-05-05 10:10:01 +05:30
Soham Kulkarni
481a886a2e
Merge pull request #39071 from KerollesFathy/fix/workspace-private-routing 2026-05-04 22:35:24 +05:30
Soham Kulkarni
4870a8070d
Merge pull request #39081 from sokumon/query-report 2026-05-04 17:47:19 +05:30
sokumon
306cd68a88 fix: handle prepared report if no previous completed prepared reports 2026-05-04 17:10:26 +05:30
Aarol D'Souza
7f7da54eca
Merge pull request #39072 from AarDG10/revert-html
fix: avoid escaping small text fields
2026-05-04 10:40:19 +05:30
AarDG10
3f59932e16 fix: avoid escaping small text fields
This is a temp. change
2026-05-04 10:18:22 +05:30
Soham Kulkarni
4d850046a8
Merge pull request #39066 from KerollesFathy/fix/desktop-modal-heading-close 2026-05-03 22:27:15 +05:30
KerollesFathy
96e61b23e2 fix(workspace): use workspace name instead of title for routing in sidebar link 2026-05-03 13:56:20 +00:00
KerollesFathy
ace15b5588 fix(workspace): use workspace name instead of title for routing after create or edit 2026-05-03 13:50:12 +00:00
frappe-pr-bot
eb8963e6ee chore: update POT file 2026-05-03 09:54:01 +00:00
KerollesFathy
32496da7eb fix(desktop-modal): close modal when clicking outside the title 2026-05-02 19:29:30 +00:00
Hussain Nagaria
328ad6c69b
Merge pull request #39043 from gajjug004/fix/file-upload-wrong-folder
fix(file): upload to current folder from File list view when inside subfolder
2026-05-02 21:30:26 +05:30
Hussain Nagaria
fca98db804
Merge pull request #39061 from frappe/ui/lh-form
chore(ux): rearrange Letter Head form
2026-05-02 10:50:37 +05:30
Hussain Nagaria
d849de25f8 chore(ux): rearrange Letter Head form 2026-05-02 10:40:01 +05:30
Ejaaz Khan
0d0ce03a51
Merge pull request #39047 from sagarvora/fix/remove-unnecessary-deep-copy-in-paste-listener
refactor: remove unnecessary deep copy in paste doc listener
2026-05-01 21:26:21 +05:30
Aarol D'Souza
e7812c738d
Revert "fix(query): unique aliasing for linked field joins" (#39053)
* Revert "fix(query): unique aliasing for linked field joins"

* fix: revert some changes
2026-05-01 18:54:34 +05:30
AarDG10
58d8859ddb fix: revert some changes 2026-05-01 18:41:02 +05:30
Aarol D'Souza
8b47c6e5a0
Revert "fix(query): unique aliasing for linked field joins" 2026-05-01 18:15:23 +05:30
Ankush Menat
b011532a7e
Merge pull request #39048 from ankush/fix_invalid_apps
fix: invalid default app
2026-05-01 13:45:36 +05:30
Ankush Menat
d9f8b24853 fix: Erase invalid default apps
and ignore at runtime in /apps handlers
2026-05-01 13:34:41 +05:30
Ankush Menat
1bdda7a803 fix: Avoid suggesting invalid " " as default app 2026-05-01 13:23:18 +05:30
Sagar Vora
0aec2e48a4 refactor: remove unnecessary deep copy in paste doc listener 2026-05-01 10:23:06 +05:30
Soham Kulkarni
1b2cf94563
fix: restrict resetting of form tours (#39026) 2026-05-01 09:25:55 +05:30
gajjug004
32588d3ec8 fix(file): upload to current folder from File list view when inside subfolder 2026-04-30 23:34:25 +05:30
Kaushal Shriwas
ec8b3d9187 test: shorten safe filters test names 2026-04-30 19:34:14 +05:30
Hussain Nagaria
a921de1fc6
Merge pull request #39030 from kaulith/fix/query-report-export-datetime-total 2026-04-30 16:53:51 +05:30
Kaushal Shriwas
e4e1c4af55 refactor: better naming and move variable inside the scope 2026-04-30 16:37:31 +05:30
Kaushal Shriwas
8ade4ce27d fix: preserve numeric-shaped docnames without losing JSON parsing of scalars 2026-04-30 15:28:55 +05:30
Kaushal Shriwas
eb76248c99 refactor: extract non-labelable fieldtypes to constant 2026-04-30 15:24:38 +05:30
Kaushal Shriwas
470964015e fix: also parse JSON-encoded scalar strings in get_safe_filters 2026-04-30 15:10:13 +05:30
Kaushal Shriwas
46fd36d542 refactor: simplify get_safe_filters guard for readability 2026-04-30 13:31:19 +05:30
Kaushal Shriwas
ede2dea043 fix(report): skip total row label for datetime/time first column 2026-04-30 13:20:16 +05:30
Kaushal Shriwas
ff83bb1473 fix: preserve docnames matching scientific notation in get_safe_filters 2026-04-30 13:15:04 +05:30
Aarol D'Souza
b7657442de
Merge pull request #39022 from AarDG10/fix-limit-offset
fix(query): use default limit when offset is used in MariaDB and SQLite w/o limit
2026-04-30 11:24:05 +05:30
AarDG10
efcd5011fa test: add test for limit offset behavior 2026-04-30 11:09:47 +05:30
AarDG10
d6adf919c9 fix(query): use default limit when offset is used in MariaDB and SQLite
MariaDB and SQLite don't allow use of offset as a standalone, limit is a must, enforcing that in QB. PostgreSQL allows use of offset w/o limit clause.
2026-04-30 10:42:35 +05:30
Soham Kulkarni
3bc0a61826
Merge pull request #39019 from sokumon/onboarding 2026-04-30 01:31:28 +05:30
sokumon
f71597fedf fix(onboarding): only update allowed fields 2026-04-30 01:01:14 +05:30
Ejaaz Khan
1a82fabed6
Merge pull request #39015 from iamejaaz/small-ui-ux-fix
fix(File): change video icon
2026-04-29 21:40:17 +05:30
Ejaaz Khan
635b464cb1 fix(File): change video icon 2026-04-29 21:20:05 +05:30
Ejaaz Khan
1a8fc0b553
Merge pull request #38998 from iamejaaz/small-ui-ux-fix
fix(File): Change remove Icon
2026-04-29 21:07:08 +05:30
Aarol D'Souza
7c93554b00
Merge pull request #38983 from AarDG10/fix-auth
fix(auth): return auth. error in case query fails
2026-04-29 19:51:30 +05:30
Shrihari Mahabal
f185f03660
Merge pull request #39008 from ShrihariMahabal/invalidate-accepted-user-invitation
fix: invalidate user invitation if already accepted
2026-04-29 19:32:28 +05:30
Shrihari Mahabal
7e45db4cec fix: invalidate user invitation if already accepted 2026-04-29 19:20:47 +05:30
Aarol D'Souza
a2b162c960
Merge pull request #38945 from AarDG10/fix-search
fix(search): escape link title field
2026-04-29 16:40:55 +05:30
Ejaaz Khan
c3c599cefa
Merge pull request #38997 from iamejaaz/38971-navigation-button
fix(ListView): add border to the right
2026-04-29 16:39:26 +05:30
Ejaaz Khan
74f8cec1c3
Merge pull request #38976 from KerollesFathy/fix/prevent-standard-report-without-developer-mode
fix(report): prevent standard report creation when developer mode is off
2026-04-29 16:36:48 +05:30
Ejaaz Khan
add397a3ed fix(File): Change delete Icon 2026-04-29 16:32:24 +05:30
Ejaaz Khan
349bdee22f refactor: fix comment 2026-04-29 16:21:06 +05:30
Ejaaz Khan
9d22550251 fix(ListView): add border to the right 2026-04-29 16:18:11 +05:30
AarDG10
72b199fbce fix(report_view): link_title should be rendered as plain text 2026-04-29 16:16:49 +05:30
Hussain Nagaria
4093ecc409
Merge pull request #38991 from gajjug004/fix/grid-paste-on-paginated-pages
fix(grid): bulk paste broken on pages beyond first
2026-04-29 16:08:33 +05:30
KerollesFathy
a1383ed98f refactor: extract standard report validation into dedicated method
Co-authored-by: Ejaaz Khan <ejaaz@frappe.io>
2026-04-29 10:23:47 +00:00
Ejaaz Khan
c2163c7028
Merge pull request #38932 from KerollesFathy/fix/create-permission-type
fix(Permission Type): disable user cannot create permission
2026-04-29 15:29:04 +05:30
KerollesFathy
acb342efad refactor: add validate_standard_report_developer_mode method
Co-authored-by: Ejaaz Khan <ejaaz@frappe.io>
2026-04-29 09:37:01 +00:00
gajjug004
62d8baa24b fix(grid): bulk paste broken on pages beyond first 2026-04-29 15:04:26 +05:30
Raheel Khan
9d5783e519
fix: remove user_doctypes limit from user type as there is no per employee user limit on desk (#38975) 2026-04-29 14:33:09 +05:30
Shrihari Mahabal
961183f5a6
Merge pull request #38942 from ShrihariMahabal/doc-follow-perm-check
fix: add perm check to document follow
2026-04-29 14:07:11 +05:30
Shrihari Mahabal
a87e2654c7 fix: add perm check on unfollow doc 2026-04-29 13:55:21 +05:30
Shrihari Mahabal
0eb922b05d
Merge pull request #38984 from ShrihariMahabal/validate-pvt-file-access
fix: validate private file access before inserting
2026-04-29 13:42:48 +05:30
Shrihari Mahabal
b33929cea8 fix: validate private file access before inserting 2026-04-29 13:31:16 +05:30
AarDG10
38537b822d fix(auth): return auth. error in case query fails
Previously was running query and throwing trace on failure, we should be raising an Auth. error if failure has occurred during auth.
2026-04-29 12:00:16 +05:30
MochaMind
73a81e0c02
fix: sync translations from crowdin (#38981) 2026-04-28 22:56:55 +02:00
Ejaaz Khan
e83608e0d5
Merge pull request #38977 from iamejaaz/38971-navigation-button
fix: default show navigation button
2026-04-28 22:11:29 +05:30
Aarol D'Souza
5f6a1d5094
Merge pull request #38973 from AarDG10/fix-query-gen
fix(query): unique aliasing for linked field joins
2026-04-28 22:10:08 +05:30
Ejaaz Khan
bd57f0501a
Merge pull request #38974 from KerollesFathy/fix/sidebar-collapse-icon-rtl
fix(sidebar): correct collapse sidebar icon direction in RTL
2026-04-28 22:04:55 +05:30
Ejaaz Khan
d4b995af04 fix: default show navigation button 2026-04-28 22:00:47 +05:30
AarDG10
2ea2c68e6e test: fix tests to accomodate new change 2026-04-28 19:46:11 +05:30
KerollesFathy
15966a78a6 fix(report): prevent standard report creation when developer mode is off 2026-04-28 13:32:46 +00:00
AarDG10
8b763e96e3 test: fix test to accomodate multi-db queries 2026-04-28 18:23:40 +05:30
KerollesFathy
f911835a5d fix(sidebar): correct collapse sidebar icon direction in RTL 2026-04-28 12:27:07 +00:00
AarDG10
fd2661160e fix(query): always alias the table when used in joins 2026-04-28 17:52:20 +05:30
Aarol D'Souza
9d683f15c7
Merge pull request #38952 from AarDG10/fix-disc-topic
fix(discussion_topic): add perm. check to submit_discussion method
2026-04-28 16:18:17 +05:30
AarDG10
a9d98723b4 test: add test to check if reply is restricted to owner 2026-04-28 15:51:47 +05:30
Soham Kulkarni
8df5ca026b
Merge pull request #38960 from sokumon/keyboard-form 2026-04-28 12:44:44 +05:30
sokumon
6e2c35b6ef fix: keyboard shortcuts on form 2026-04-28 12:29:15 +05:30
Aarol D'Souza
3b8d67ae11
Merge pull request #38941 from AarDG10/fix-email-acc
fix(email_account): add perm. check to set_email_password
2026-04-28 12:15:52 +05:30
Soham Kulkarni
a75d243071
Merge pull request #38953 from sokumon/desktop-folder 2026-04-28 12:10:58 +05:30
sokumon
63724e1e3a fix: small jitter while opening an folder icon 2026-04-28 11:56:51 +05:30
AarDG10
befd7f313c fix(discussion_topic): add perm. check to submit_discussion method
Users should not be able to edit someone else's replies. Forbidding it w/ this check.
2026-04-28 11:19:10 +05:30
Aarol D'Souza
cc519fd4ad
Merge pull request #38948 from AarDG10/fix-std-macros
fix(standard_macros): escape fields in standard print format template
2026-04-28 08:25:46 +05:30
AarDG10
9bcac62d98 fix(standard_macros): escape fields in standard print format template
Escaping on output, and reverting changes made in formatters.py.
2026-04-28 08:12:16 +05:30
MochaMind
643b405eb2
fix: sync translations from crowdin (#38946) 2026-04-27 22:40:12 +02:00
Ejaaz Khan
94120badd5
Merge pull request #38846 from Shllokkk/letter-head-fix
feat: add custom_css field in letterhead
2026-04-27 23:57:37 +05:30
Ejaaz Khan
ebdd472ba6
Merge pull request #38880 from Abdeali099/delete-property-setters
feat: Add bulk delete utility for property setters
2026-04-27 21:59:46 +05:30
Soham Kulkarni
8f288a6a2d
Merge pull request #38877 from sokumon/form-builder-ui 2026-04-27 19:27:22 +05:30
sokumon
02a422be81 fix: icons on field container 2026-04-27 18:31:38 +05:30
Shrihari Mahabal
56c602e94d fix: add perm check to document follow 2026-04-27 18:00:47 +05:30
AarDG10
dd9450dc46 fix(email_account): add perm. check to set_email_password
Not too certain why a perm. check was skipped here, adding nonetheless.
2026-04-27 17:34:47 +05:30
Ankush Menat
54145369f4
ci: remove a lot of dead CI code (#38940)
- Selectively upload coverage only when captured
2026-04-27 12:04:29 +00:00
KerollesFathy
9c8eefab7b fix(Permission Type): disable user can not create 2026-04-27 10:31:04 +00:00
Ejaaz Khan
242e7b55ca
Merge pull request #38914 from KerollesFathy/fix-getting-started
fix(sidebar): show "Getting Started" only when sidebar expanded
2026-04-27 15:57:41 +05:30
KerollesFathy
3b5efd28ae refactor: use padding 2026-04-27 10:07:59 +00:00
KerollesFathy
f1c0d75c94 fix(sidebar): add margin to "Getting Started" onboarding link 2026-04-27 09:45:15 +00:00
Ankush Menat
27712f3c76
perf: Avoid ordering for link field checks (#38928) 2026-04-27 15:06:30 +05:30
Ejaaz Khan
e13017794b
Merge pull request #38523 from frappe/38448-property-to-include-fields-in-default-import-template
feat: property to include fields in default import template
2026-04-27 14:55:38 +05:30
Ejaaz Khan
e6daaa6c48
Merge pull request #38567 from frappe/38159-allow-bulk-edit-in-child-tables
feat: allow Bulk Edit in Child Table
2026-04-27 14:50:55 +05:30
Sumit Jain
f7a344857f fix(grid): enhance phone field handling in bulk edit modal for child tables 2026-04-27 13:32:20 +05:30
Sumit Jain
38bbe0a3a3 fix(grid): improve phone selection in bulk edit modal for child tables 2026-04-27 12:45:49 +05:30
Aarol D'Souza
cdc6dca582
Merge pull request #38835 from AarDG10/fix-formatters
fix(formatters)!: escape input fields
2026-04-27 12:26:38 +05:30
AarDG10
7693e6970d test(formatter): add test to check escaping 2026-04-27 12:06:20 +05:30
Sumit Jain
25c19683db feat(grid): add tests for bulk edit functionality in child tables 2026-04-27 11:49:45 +05:30
Abdeali Chharchhoda
8d328e2f94 refactor: Simplify property setter deletion logic and enforce required fields 2026-04-27 10:43:24 +05:30
Sumit Jain
440c94ff9e feat(customize_form): add 'in_import_template' field option to customize form and field 2026-04-27 10:43:04 +05:30
MochaMind
374fa3cf6f
fix: sync translations from crowdin (#38919)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Slovenian translations

* fix: Turkish translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Burmese translations

* fix: Norwegian Bokmal translations

* fix: German translations

* fix: Hungarian translations

* fix: Russian translations

* fix: Serbian (Cyrillic) translations

* fix: Swedish translations

* fix: Chinese Simplified translations

* fix: Croatian translations

* fix: Bosnian translations

* fix: Serbian (Latin) translations

* fix: Esperanto translations
2026-04-27 10:29:11 +05:30
Ejaaz Khan
5630086f43
Merge pull request #38916 from KerollesFathy/fix/email-queue-update-status
fix(EmailQueue): ensure communication exists before updating delivery status
2026-04-27 10:19:43 +05:30
Ejaaz Khan
17a4b7a83d
Merge pull request #38856 from KerollesFathy/fix/update-link-field-in-multi-select-dialog
fix(MultiSelectDialog): refresh results on Link field selection without requiring blur
2026-04-27 10:18:09 +05:30
KerollesFathy
615b2c5e8d fix(EmailQueue): ensure communication exists before updating delivery status 2026-04-26 12:11:25 +00:00
MochaMind
19f68c98bb
chore: update POT file (#38911) 2026-04-26 17:31:22 +05:30
Ejaaz Khan
323ee5beba
Merge pull request #38907 from iamejaaz/small-ui-ux-fix
chore: update datatble to 1.20.3
2026-04-26 17:31:00 +05:30
KerollesFathy
052db75961 fix(sidebar): show "Getting Started" only when sidebar expanded 2026-04-26 11:01:52 +00:00
Ejaaz Khan
328d4763e5 chore: update datatble to 1.20.3 2026-04-26 11:08:44 +05:30
Ejaaz Khan
9a22cabd95
Merge pull request #38905 from iamejaaz/small-ui-ux-fix
fix(QueryReport): change toggle cursor to pointer
2026-04-26 10:33:43 +05:30
Ejaaz Khan
007328ae89 fix(QueryReport): change toggle cursor to pointer 2026-04-26 10:10:31 +05:30
Ejaaz Khan
614da3ee77
Merge pull request #38904 from iamejaaz/small-ui-ux-fix
fix(datatable): left border visibility
2026-04-26 10:09:22 +05:30
Ejaaz Khan
7c77b1c6b1 fix(datatable): left border visibility 2026-04-26 10:03:06 +05:30
Ejaaz Khan
dcdc2eeae9
Merge pull request #38899 from iamejaaz/create-new-shortcut
feat: add crtl+b to create new doc
2026-04-26 08:31:21 +05:30
Ejaaz Khan
0905493518
Merge pull request #37852 from KerollesFathy/fix-copy-doc-title
fix(form_sidebar): copy document title
2026-04-26 00:09:36 +05:30
Ejaaz Khan
5d19b1998f feat: add crtl+b to create new doc 2026-04-25 23:57:01 +05:30
Ejaaz Khan
5b4ddb2236
Merge pull request #38896 from iamejaaz/small-ui-ux-fix
fix(sidebar): notification panel and icon hover state
2026-04-25 16:27:45 +05:30
Ejaaz Khan
6216cda60e fix(sidebar): hover state of shortcut items 2026-04-25 14:48:28 +05:30
Ejaaz Khan
02e764cd4c fix: notification panel issue on close sidebar 2026-04-25 14:47:36 +05:30
Aarol D'Souza
590b47c596
Merge pull request #38892 from AarDG10/fix/db-query-self-ref-alias
fix(db_query): unique alias on self-referential Link joins
2026-04-25 13:02:25 +05:30
Shrihari Mahabal
2e0a8f19f1
Merge pull request #38891 from ShrihariMahabal/skip-http-validation-for-bg-tasks
fix: skip http validation for servers scripts in background tasks
2026-04-25 12:51:37 +05:30
AarDG10
455595f2f5 refactor(test): minor refactor and added test to check query 2026-04-25 12:42:57 +05:30
vijayanrxbb
55e342bbd5 test(db_query): assert self-referential Link joins don't collide on alias
Mirrors test_ambiguous_linked_tables but with a DocType whose Link
fields point back to itself, covering the path where pypika's '2'
auto-alias collides on the second self-join.
2026-04-25 12:42:57 +05:30
vijayanrxbb
383b24e0f9 fix(db_query): unique alias on self-referential Link joins
When a DocType has two or more Link fields pointing back to itself and
list view tries to join them, pypika auto-aliased both joins with the
same '<table>2' suffix, producing a MySQL 1066 'Not unique table/alias'
error.

Alias each self-referential join with 'tab<doctype>_<link_fieldname>'
so every join gets a unique alias, and use the same aliased table in
apply_select so SELECT references resolve correctly.
2026-04-25 12:42:57 +05:30
Shrihari Mahabal
234b0722a2 fix: skip http validation for servers scripts in background tasks 2026-04-25 12:19:18 +05:30
Ankush Menat
bf69eebca3
ci: Skip type checks in server tests (#38890) 2026-04-25 06:40:10 +00:00
Ankush Menat
7a565de242
ci: Tweak coverage (again) (#38889)
* ci: Skip other dbs in coverage computation

* ci: Add accurate coverage tracking (again)
2026-04-25 11:59:22 +05:30
Ejaaz Khan
916d04ae74
Merge pull request #38885 from iamejaaz/small-ui-ux-fix
fix(ListView): content overlow in small screens
2026-04-24 22:49:04 +05:30
Ejaaz Khan
58667ec285 fix(ListView): content overlow in small screens 2026-04-24 22:45:40 +05:30
Aarol D'Souza
b42fc9baec
Merge pull request #38882 from frappe/revert-38815-fix-client
Revert "fix(client): add stronger checks in save and set_value endpoints"
2026-04-24 20:19:59 +05:30
Aarol D'Souza
74f125c360
Revert "fix(client): add stronger checks in save and set_value endpoints" 2026-04-24 20:09:37 +05:30
Abdeali Chharchhoda
fdf74f32b9 test: Add test for bulk deletion of property setters 2026-04-24 18:32:58 +05:30
Abdeali Chharchhoda
2de9ecc033 refactor: Add bulk delete utility for property setters 2026-04-24 18:32:41 +05:30
Shrihari Mahabal
6284ad13cd
Merge pull request #38876 from ShrihariMahabal/check-perm-on-user-email-awaiting
fix: add perm check on user email awaiting
2026-04-24 18:12:59 +05:30
Shrihari Mahabal
c19dd276ba fix: add perm check on user email awaiting 2026-04-24 17:10:50 +05:30
Sumit Jain
c7f72cc315 fix(dialog): prevent help text display when primary button is focused or active 2026-04-24 17:04:34 +05:30
Sumit Jain
299a15652f feat(customize_form): add 'Allow Bulk Edit' option for child table fields 2026-04-24 16:53:43 +05:30
Ankush Menat
098a0851c6
ci: Fix coverage reporting (again) (#38849)
* chore: remove _decorate_all_methods_and_functions_with_type_checker

No one understands this runtime magic anymore.

* build: Bump coverage.py to latest

* test: Skip github in coverage reporting

* test: Print traceback from all threads when test is stuck

* ci: Enable coverage in server side tests

* ci: Always enable coverage

It's cheap in recent python versions, our reasons for selectively
disabling aren't valid anymore.

* ci: Disable stderr capturing

* ci: Use default buffer behaviour in unittest runner

* ci(coverage): Set concurrency to multiprocessing

We do use multiprocessing, perhaps the patches aren't concurrectly
handled?

* ci(coverage): Try parallel run

* fix: Apply subprocess patch

* ci: Don't start web server with coverage

Causes deadlock for some reason. We don't actually report it either.

* ci: only submit UI coverage if ran

* test: remove aggresive stuck test checking

* ci: disable UI coverage

(for now)
2026-04-24 16:05:14 +05:30
Ejaaz Khan
d9d35fa4ad
Merge pull request #38866 from iamejaaz/small-ui-ux-fix
fix(ListView): Small UI & UX fixes related to navbar
2026-04-24 15:47:24 +05:30
RitvikSardana
b484cf0136
fix: pytz to filter out deprecated timezones (#38751) 2026-04-24 14:06:40 +05:30
Sumit Jain
bbfcaf67f2 fix(grid): clarify bulk edit success message for selected rows 2026-04-24 14:02:33 +05:30
Sumit Jain
aea2f722e7
Merge branch 'develop' into 38159-allow-bulk-edit-in-child-tables 2026-04-24 13:44:28 +05:30
Sumit Jain
42470e9201 fix(multicheck): move warning icon style to checkbox stylesheet 2026-04-24 13:43:15 +05:30
Sumit Jain
75c43493a8 refactor: change variable name from hide_name_for_insert_when_not_set_by_user ton hide_name_for_autoname 2026-04-24 13:35:24 +05:30
Sumit Jain
78b81fc7cf refactor: Rename 'include_in_import_template' to 'in_import_template' 2026-04-24 13:34:42 +05:30
Shllokkk
006d0e1754 fix: minor suggested fixes 2026-04-24 13:20:47 +05:30
Sumit Jain
df2947b546 fix(multicheck): move warning icon style to checkbox stylesheet 2026-04-24 13:20:33 +05:30
Ejaaz Khan
f7a0be068b fix: set max-width to list title 2026-04-24 13:12:50 +05:30
Ejaaz Khan
d68a814e02 fix(ListView): hide indicator on listview 2026-04-24 13:01:31 +05:30
Ejaaz Khan
8054844193
Merge pull request #38767 from Abdeali099/delete-custom-fields-utility
feat: added a method to delete custom_fields
2026-04-24 12:48:51 +05:30
Abdeali Chharchhoda
1d571827cf chore: clear cache after deleting custom fields 2026-04-24 12:01:34 +05:30
Ejaaz Khan
398e4879d5
Merge pull request #38864 from iamejaaz/bum-datatable
chore: update datatble to 1.20.2
2026-04-24 11:25:06 +05:30
Ejaaz Khan
43d7497588 chore: update datatble to 1.20.2 2026-04-24 11:15:36 +05:30
Ankush Menat
1f9015a9c2
fix: Re-add rate limit on blog comments (#38862) 2026-04-24 05:09:14 +00:00
Kerolles Fathy
13230516b8
Merge pull request #38858 from devdiogenes/edit_section-translatable
fix: Make Edit Section title translatable in Print format Builder
2026-04-24 01:09:43 +03:00
Kerolles Fathy
3d414812c8
Merge pull request #38857 from devdiogenes/fix-condition-examples-translatable
fix: Makes Condition Examples title translatable
2026-04-24 01:08:52 +03:00
KerollesFathy
8e6003afb2 fix(multi_select_dialog): refresh results on Link field selection without requiring blur 2026-04-23 20:09:28 +00:00
devdiogenes
984ebe5ae7 fix: Make Edit Section title translatable in Print format Builder 2026-04-23 19:47:39 +00:00
Ejaaz Khan
25c95c7f9d
Merge pull request #38831 from s-aga-r/fix-66079
fix(SMTP): config to disable EHLO after AUTH
2026-04-23 23:32:52 +05:30
Ejaaz Khan
be6e85fe9e
Merge pull request #38748 from barredterra/fix/test-runner-missing-doctype
fix(tests): skip uninstalled doctypes in test record dependency walk
2026-04-23 23:21:48 +05:30
Ejaaz Khan
778aa12feb
Merge pull request #38393 from barredterra/defer-doctype-export
fix: defer DocType exports until after save response
2026-04-23 23:19:16 +05:30
Saqib Ansari
a8c373a2f4
Merge pull request #38576 from nextchamp-saqib/concurrent_limit
feat: add `@frappe.concurrent_limit()` decorator
2026-04-23 22:55:59 +05:30
Saqib Ansari
01de388c26
Merge pull request #38800 from nextchamp-saqib/qb-json-functions
feat: add JSON functions in query builder
2026-04-23 22:50:47 +05:30
Aarol D'Souza
f10abb4e06
Merge pull request #38750 from pratikb64/fix/delete-utils
fix: add get_dynamic_linked_docs & get_linked_docs utils
2026-04-23 20:27:35 +05:30
AarDG10
1257e0db42 refactor(delete_doc): minor refactor in qb usage 2026-04-23 20:13:42 +05:30
AarDG10
785c85e6a5 fix(formatters): escape fields
Escaped fields so as to render them as text nodes.
2026-04-23 20:10:01 +05:30
Pratik
05532b3697 refactor: remove for loop while raising exception 2026-04-23 18:52:54 +05:30
Hussain Nagaria
1b07869248
Merge pull request #38828 from imgullu786/fix/workspace-save 2026-04-23 17:15:34 +05:30
Shllokkk
72cdae85e7 fix: add css handling for letterheads for report printing 2026-04-23 16:21:49 +05:30
Shllokkk
193c2c200f fix: add css handling for letterheads for doctype printing 2026-04-23 16:20:45 +05:30
Shllokkk
0ab6840d1d feat(letterhead): introduce custom_css field to move styling out of html fields and to prevent scripts in html fields 2026-04-23 16:19:22 +05:30
Shrihari Mahabal
620541d97c
Merge pull request #38843 from ShrihariMahabal/event-permissions
fix: check permissions for getting and updating events
2026-04-23 15:51:00 +05:30
Shrihari Mahabal
b4c2a6c617 fix: check permissions for getting and updating events 2026-04-23 15:38:32 +05:30
Aarol D'Souza
58badf002c
Merge pull request #38815 from AarDG10/fix-client
fix(client): add stronger checks in save and set_value endpoints
2026-04-23 14:06:58 +05:30
Md Gulam Gaush
be0cf977a2 fix: Page not found error when saving public workspace with multi word name 2026-04-23 11:49:24 +05:30
Abdeali Chharchhoda
9a75ff6fd3 test: enhance delete_custom_fields test to cover multiple deletion methods 2026-04-23 11:26:47 +05:30
Saqib Ansari
588112b833
refactor: bring back collapse button (#38742) 2026-04-23 11:26:20 +05:30
s-aga-r
7f355b3f53 fix(SMTP): config to disable EHLO after AUTH 2026-04-23 11:25:54 +05:30
Abdeali Chharchhoda
7fd4451e96 refactor: enhance delete_custom_fields function to support bypassing hooks 2026-04-23 10:51:52 +05:30
Saqib Ansari
9d3f410037 feat: add tests for JSON functions 2026-04-23 10:29:58 +05:30
AarDG10
8825691746 fix(client): add blocklist for save endpoint
These fields are standard and shouldn't be editable through an endpoint. Discarded some of them since they already validate, these don't.
2026-04-23 10:16:43 +05:30
Ejaaz Khan
1cf89c9bc9
Merge pull request #38813 from devdiogenes/fix-translate-docname-share-dialog
fix: Make docname translatable in share dialog
2026-04-23 00:23:08 +05:30
devdiogenes
95d0fecee7 fix: Makes Condition Examples title translatable 2026-04-22 18:50:01 +00:00
MochaMind
fdaa45754f
fix: sync translations from crowdin (#38820) 2026-04-22 20:23:05 +02:00
Ejaaz Khan
3d8c444c8c
Merge pull request #38819 from frappe/revert-38249-fix/serial-no-style-on-report
Revert "fix(report): enhance the visibility of serial-no at first column"
2026-04-22 23:31:42 +05:30
Ejaaz Khan
cbfeb13753
Revert "fix(report): enhance the visibility of serial-no at first column (#38…"
This reverts commit 8509c79877.
2026-04-22 23:30:16 +05:30
Ejaaz Khan
6293525d97
Merge pull request #38805 from iamejaaz/64416-default-app
fix(BulkEdit): Default app options not populating
2026-04-22 22:58:01 +05:30
Kerolles Fathy
215a4231f5
Merge pull request #38816 from KerollesFathy/translate-no-tags
fix(filter): update condition to use translated string for "No Tags"
2026-04-22 19:10:54 +02:00
KerollesFathy
1722a6204c fix(filter): update condition to use translated string for "No Tags" 2026-04-22 16:40:49 +00:00
AarDG10
616a17c3ec refactor(client): add stronger checks
Previous code was very passive for dicts., this fixes that by parsing and then checking membership.
2026-04-22 21:13:38 +05:30
Shrihari Mahabal
c30ea141b6
Merge pull request #38810 from ShrihariMahabal/hide-user-login
fix: hide user login
2026-04-22 20:03:05 +05:30
Shrihari Mahabal
d680d21646 refactor: remove unnecessary msgprint 2026-04-22 19:43:23 +05:30
devdiogenes
3c95598c1d fix: Make docname translatable in share dialog 2026-04-22 13:43:33 +00:00
Shrihari Mahabal
fe7b1ca7bd fix: dont show user does not exist error 2026-04-22 17:59:48 +05:30
Ejaaz Khan
b17f60a7fa fix(BulkEdit): Default app options not populating 2026-04-22 17:47:44 +05:30
Aarol D'Souza
e9d579125d
Merge pull request #38802 from AarDG10/fix-queue
fix(queue): correct the conditional check in retry_sending_emails
2026-04-22 17:34:14 +05:30
Henning Wendtland
3571469cea fix(queue): correct the conditional check in retry_sending_emails 2026-04-22 17:25:19 +05:30
Saqib Ansari
11efbde9b9 feat: add JSON functions in query builder 2026-04-22 16:22:11 +05:30
Saqib Ansari
53e7e34948 refactor: make token initialization simple 2026-04-22 16:20:54 +05:30
Saqib Ansari
850cc58664 fix: clear_cache for shared cache 2026-04-22 16:20:54 +05:30
Saqib Ansari
757f283eea feat: add get_stats function to retrieve concurrency limits 2026-04-22 16:20:54 +05:30
Saqib Ansari
0064eb80b4 fix: support shared RedisSemaphores for concurrency limits 2026-04-22 16:20:54 +05:30
Saqib Ansari
7f78cd25f9 refactor: extract RedisSemaphore into redis_semaphore.py 2026-04-22 16:20:54 +05:30
Saqib Ansari
65965b9c44 fix: use site_cache as clear_cache is broken for redis_cache 2026-04-22 16:20:54 +05:30
Shrihari Mahabal
1b8f6cddbb
Merge pull request #38796 from ShrihariMahabal/escape-icon-color
fix: escape icon and color fields
2026-04-22 15:02:39 +05:30
Shrihari Mahabal
b593285b56 fix: escape icon and color fields 2026-04-22 14:47:11 +05:30
Aarol D'Souza
b828efe1b5
Merge pull request #38791 from iamejaaz/pdfkit-parsing-meta
fix: disable meta tag parsing in pdfkit
2026-04-22 13:45:06 +05:30
Soham Kulkarni
c7bfffd7e0
Merge pull request #38743 from sokumon/toggle-chart 2026-04-22 12:12:26 +05:30
Ejaaz Khan
69018ad4b5 fix: disable meta tag parsing in pdfkit
Co-authored-by: Ankush <ankush@users.noreply.github.com>
2026-04-22 12:11:00 +05:30
Aarol D'Souza
873362830a
Merge pull request #38790 from AarDG10/ci-reminder
ci(workflow): add backport reminder
2026-04-22 12:03:40 +05:30
AarDG10
ac469a1bae ci(workflow): add backport reminder
Can sometimes forget that a PR was deferred from being backported, adding reminder bot to remind after 2 weeks.
2026-04-22 11:46:32 +05:30
Raffael Meyer
068f6b7699
chore: update translations (#38789) 2026-04-22 01:28:41 +00:00
Raffael Meyer
a75373eab2
chore: update translations (#38788) 2026-04-22 00:44:08 +00:00
Raffael Meyer
38102d45e6
chore: update translations (#38786) 2026-04-21 20:37:53 +00:00
MochaMind
f3c6b8694d
fix: sync translations from crowdin (#38782)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Slovenian translations

* fix: Turkish translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Burmese translations

* fix: Norwegian Bokmal translations

* fix: German translations

* fix: Hungarian translations

* fix: Russian translations

* fix: Serbian (Cyrillic) translations

* fix: Swedish translations

* fix: Chinese Simplified translations

* fix: Croatian translations

* fix: Bosnian translations

* fix: Serbian (Latin) translations

* fix: Esperanto translations
2026-04-21 23:09:27 +05:30
Aarol D'Souza
953d9e4867
Merge pull request #38781 from AarDG10/fix-datetime
fix(datetime): set lastSelectedDate in datetime picker
2026-04-21 23:00:32 +05:30
AarDG10
e2dd7bc8b0 fix(datetime): set lastSelectedDate in datetime picker 2026-04-21 22:59:16 +05:30
Shrihari Mahabal
81259709a7
Merge pull request #38774 from ShrihariMahabal/prevent-single-virtual-doctype-persistence
fix: prevent persistence of virtual single doctypes
2026-04-21 21:53:49 +05:30
Abdeali Chharchhodawala
690826ff9b
feat!: faster generation and formatting utils for excel exports (#36323)
* feat: Style builder for report xlsx formatting

* fix: update report to use direct import for query report execution

* refactor: simplify module method retrieval in report execution

* feat: get xlsx styles for report

* refactor: enhance XLSXStyleBuilder with currency formatting and default style registration

* feat: add xlsxwriter dependency for enhanced XLSX report generation

* refactor: enhance XLSXStyleBuilder with improved style registration and formatting methods

* feat: enhance XLSX export functionality with improved styling and metadata support

* refactor: default formatting of currency

* chore: remove some typo

* feat: update make_xlsx function to use xlsxwriter for improved Excel file generation and styling

* perf: some micro optimisations

* refactor: inline generator back and improve condition

* refactor: replace frappe.request_cache with functools.cache

* fix: handle styling in email

* fix: fix old test case to handle styles in export

* refactor: enhance XLSX style handling and registration methods

* refactor: improve currency formatting logic

* fix: update make_xlsx to use constant_memory for large datasets and improve row style handling

* fix: handle None style_id in XLSXStyleBuilder methods to prevent errors

* fix: include owner field with proper doctype naming

* fix: set default date format in XLSX workbook creation

* fix: pass applied filters to metadata

* fix: getting accurate field info for report view exporting

* chore: Minor changes

* feat: add function to generate default XLSX styles for exports

* feat: integrate default XLSX styles into builder report export functionality

* feat: styles on export docs xlsx

* feat: enhance make_xlsx function to support file path saving

* feat: add make_xls function for creating Excel files in old format and improve sheet name sanitization

* fix: handle default date formatting

* refactor: changes xlsx builder usage

* refactor: update xlsx style builder usage

* refactor: enhance field info retrieval with default field support

* fix: handle update key in report data

* refactor: enhance get_field_info to include options and improve label retrieval

* fix: improve error handling for unsupported file formats and ensure applied filters are set correctly

* refactor: update XLSX header index handling and improve metadata structure

* fix: handle currency formatting in reportview export

* fix: update default date format to datetime format in XLSX creation

* fix: update serial number field in auto email report to use 'sr' instead of 'idx'

* fix: enhance XLSX styling by adding right alignment for specific field types

* chore: remove unused code

* fix: update XLSXMetadata attributes for improved report styling options

* perf: further improve currency styling

* fix: correct column index mapping in XLSX export header

* refactor: optimize indentation style registration in XLSXStyleBuilder

* perf: improve apply_indentations

* fix: reduce more attr lookup

* refactor: remove duplication

* fix: use report name in XLSX export instead of hardcoded title

* fix: remove ignore_visible_idx from XLSXMetadata

* fix: review

* fix: update XLSX style fetching logic in build_xlsx_data function

* fix: add right alignment to date, time, and datetime styles in XLSXStyleBuilder

* fix: simplify number format handling in XLSXStyleBuilder

* fix: register common styles in XLSXStyleBuilder for improved style management

* test: add tests for XLSX styles structure and fieldtype column styles in XLSXStyleBuilder

---------

Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
2026-04-21 19:07:43 +05:30
Shrihari Mahabal
07dd2fd9dc fix: prevent persistence of virtual single doctypes 2026-04-21 19:04:11 +05:30
Abdeali Chharchhoda
a0240a3d18 chore: minor fix 2026-04-21 18:33:46 +05:30
Kerolles Fathy
9f5b45167d
Merge pull request #38766 from frappe/revert-38249-fix/serial-no-style-on-report
Revert "fix(report): enhance the visibility of serial-no at first column"
2026-04-21 14:52:07 +02:00
Abdeali Chharchhoda
0ed3651767 test: add test for delete_custom_fields function 2026-04-21 18:21:00 +05:30
Abdeali Chharchhoda
eb8e683c26 feat: add delete_custom_fields function to remove custom fields from doctypes 2026-04-21 18:20:16 +05:30
Kerolles Fathy
56dbad7715
Revert "fix(report): enhance the visibility of serial-no at first column (#38…"
This reverts commit 8509c79877.
2026-04-21 14:45:04 +02:00
Soham Kulkarni
e516f716bf
Merge pull request #38764 from sokumon/remove-icon-close
fix: replace icon-close with icon-x
2026-04-21 17:36:05 +05:30
sokumon
32aaaa7abe fix: replace icon-close with icon-x 2026-04-21 17:01:39 +05:30
sokumon
a8ca40b444 fix(dialog): add a flag to include default value 2026-04-21 16:58:54 +05:30
MochaMind
7d49a515b0
chore: update POT file (#38714) 2026-04-21 16:22:39 +05:30
Ejaaz Khan
19eb07ffc4
Merge pull request #38752 from iamejaaz/bum-datatable
chore: update datatble to 1.20.1
2026-04-21 12:47:50 +05:30
Ejaaz Khan
48f48b6207 chore: update datatble to 1.20.1 2026-04-21 12:31:27 +05:30
Aarol D'Souza
4358f5bd44
Merge pull request #38740 from AarDG10/fix-backup
fix(response): harden download_backup
2026-04-21 12:11:08 +05:30
Ejaaz Khan
486d56934b
Merge pull request #38741 from KerollesFathy/fix-hide-permissions-tab
fix(doctype): show Permissions tab only when doctype is not a child table
2026-04-21 06:59:48 +05:30
barredterra
38e140df22 fix(tests): skip uninstalled doctypes in test record dependency walk
The test runner walks link-field dependencies recursively to pre-generate
test records via `get_missing_records_doctypes`. If any DocType in the
transitive link graph belonged to an app not installed on the test site,
the walk crashed with `DoesNotExistError`, aborting the entire suite
before a single test ran.

Treat such link targets as dead-end leaves instead:

- `get_modules` now returns `(None, None)` when the DocType row does not
  exist, instead of falling through into `load_doctype_module` which
  raises.
- `get_missing_records_doctypes` checks for `module is None`, logs a
  warning naming the parent DocType that linked to it, and returns
  without descending further.

This restores the ability to run downstream test suites that link
(directly or transitively) to optional/uninstalled apps without forcing
every CI environment to know the full transitive link graph.

Fixes #38747
2026-04-21 01:49:08 +02:00
Raffael Meyer
166bb914c1
fix: set autocomplete attribute for password fields in user and setup wizard forms (#38744) 2026-04-20 23:09:49 +00:00
sokumon
e33097fd35 fix(dialog): send default value in dialog 2026-04-21 02:48:53 +05:30
sokumon
249adb0680 fix: spacing between group by label 2026-04-21 01:13:13 +05:30
KerollesFathy
5c7d28a826 fix(doctype): show Permissions tab only when doctype is not a child table 2026-04-20 14:16:10 +00:00
AarDG10
0c660477ee fix(response): harden download_backup
Made use of util `check_path_safety` to ensure sandboxing.
2026-04-20 18:58:21 +05:30
AarDG10
7c9ce26469 feat(utils): add util to ensure sandboxing
This util can be used in places where sandboxing is needed.
2026-04-20 18:49:42 +05:30
Soham Kulkarni
ec3922e903
Merge pull request #38738 from sokumon/sidebar-history 2026-04-20 18:10:32 +05:30
sokumon
f083bdcb48 fix: add a check if sidebar_item_map exists 2026-04-20 18:02:20 +05:30
Soham Kulkarni
c8e0a89b1c
Merge pull request #38353 from sokumon/sidebar-history 2026-04-20 17:28:30 +05:30
Abdeali Chharchhodawala
3796860c92
fix: simplify total row calculation logic in query report (#38677) 2026-04-20 10:38:34 +00:00
Soham Kulkarni
57a94ca566
Merge pull request #38726 from sokumon/default-workspace 2026-04-20 14:28:25 +05:30
Ejaaz Khan
9ead794803
Merge pull request #38730 from Nihantra-Patel/fix-report-letterhead-validation
fix: skip report letter head validation when no letter head is set
2026-04-20 12:54:01 +05:30
Nihantra C. Patel
0cefdf0f8d
fix: formatting 2026-04-20 12:44:34 +05:30
Nihantra C. Patel
c36dc287b4
fix: skip report letter head validation when no letter head is set 2026-04-20 12:36:44 +05:30
Ejaaz Khan
58ef5aded0
Merge pull request #38724 from AarDG10/fix-bulk-paste
fix(table): fix bulk paste in child tables
2026-04-20 11:14:30 +05:30
sokumon
7a73a23e4a fix: remove dead code regarding default workspace 2026-04-20 02:23:14 +05:30
sokumon
767099268a fix: consider default workspace after login 2026-04-20 01:29:22 +05:30
AarDG10
a5118dcb9d fix(table): fix bulk paste in child tables 2026-04-19 22:10:42 +05:30
Hussain Nagaria
465aa38cba
Merge pull request #38608 from kaulith/fix/workspace-sidebar-empty-module-visibility 2026-04-19 19:39:35 +05:30
Hussain Nagaria
f1755daab9
Merge pull request #38536 from kaulith/fix/webform-hidden-mandatory-validation 2026-04-19 17:02:40 +05:30
Kaushal Shriwas
87b0824031 fix: skip hidden and mandatory check when allow_incomplete is set 2026-04-19 16:49:38 +05:30
MochaMind
343d55a4a7
fix: sync translations from crowdin (#38710)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Slovenian translations

* fix: Turkish translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Burmese translations

* fix: Norwegian Bokmal translations
2026-04-19 01:26:25 +02:00
Ejaaz Khan
843e396b44
Merge pull request #38466 from kaulith/fix/role-profile-not-visible-in-user-list-view
fix: sync role_profile_name for user list view display
2026-04-18 23:50:11 +05:30
Ejaaz Khan
6ebe8e2b8d
Merge pull request #38658 from kaulith/feat/sidebar-notification-unread-count
feat: show unread notification count in sidebar
2026-04-18 23:49:09 +05:30
Ejaaz Khan
c6ae260f0d
Merge pull request #38708 from KerollesFathy/fix-allow-clearing-link-fields
fix(link): Allow clearing link fields
2026-04-18 23:44:47 +05:30
KerollesFathy
f84190685f fix(link): ensure clear button state is a boolean value 2026-04-18 14:33:56 +00:00
Kaushal Shriwas
4976b4554d chore: merge develop into feat/sidebar-notification-unread-count 2026-04-18 20:01:40 +05:30
Saqib Ansari
75bae453ac
fix(prepared_report): handle missing attachments in get_prepared_data method (#38449) 2026-04-18 19:28:41 +05:30
Kerolles Fathy
63b8d78075
Merge pull request #38703 from UmakanthKaspa/fix/link-clear-icon-align
fix: Align × and right arrow icons in link field
2026-04-18 15:49:08 +02:00
Saqib Ansari
d618a88f01 feat: derive concurrency limit from gunicorn master's cmdline
Co-authored-by: Copilot <copilot@github.com>
2026-04-18 15:37:14 +05:30
Saqib Ansari
4eafb38f98 test: rewrite concurrent_limit tests to test through public interface 2026-04-18 14:58:47 +05:30
Saqib Ansari
033d49b488 fix: add TTL to capacity key so pool self-heals after worker crash
If a gunicorn worker is killed (SIGKILL, OOM) while holding a token, the
token is never returned to the pool. With no TTL on the capacity key,
`setnx` would never fire again, so the pool shrinks permanently — with
`limit=3` you silently end up at `limit=2`, then `limit=1`, etc.

Set a 1-hour TTL (`_CAPACITY_KEY_TTL`) on the capacity key via the
`NX EX` form of SET in the Lua init script. When the key expires the next
request re-initializes the pool to full capacity, so the semaphore is
self-healing without manual Redis key deletion.
2026-04-18 14:26:17 +05:30
Saqib Ansari
8589f26ce9 fix: atomically initialize token pool via Lua script in _ensure_tokens
Replace the `setnx` + pipeline pair with a Lua script evaluated in a
single round-trip. The prior approach had a race window: between the
`SET NX` succeeding and the `MULTI/EXEC` pipeline running, a concurrent
worker could BLPOP from the list just before `DEL` wiped it — losing
tokens permanently. A process crash in that window left the capacity flag
set but the token list empty, breaking the semaphore with no recovery path.

The Lua script makes the check-and-initialize atomic: Redis executes it as
a single unit with no interleaving, so the race window is closed.
2026-04-18 14:25:30 +05:30
Saqib Ansari
e8c7eb946b refactor: rewrite concurrent_limit to use LIST + BLPOP semaphore
Replace the INCRBY-based polling loop with a proper token pool backed by
a Redis LIST. BLPOP blocks until a token is available instead of sleeping
and retrying, which is more efficient and avoids the check-then-act race
of the old counter approach.

Other fixes bundled in:
- Add `blpop` and `setnx` wrappers to `RedisWrapper` so all key prefixing
  goes through `make_key` consistently
- Cache `_default_limit()` result with `@redis_cache(shared=True)` to
  avoid importing `multiprocessing` on every request
- Fix `limit=0` edge case: use `is not None` guard instead of falsy check
- Guard `_release()` against pushing the `"fallback"` token back into the
  pool when Redis was unavailable during acquire
2026-04-18 14:21:33 +05:30
UmakanthKaspa
b1d7d480fd fix: align × and → icons in link field 2026-04-18 07:57:14 +00:00
Aarol D'Souza
11066591ed
Merge pull request #38643 from AarDG10/fix-page
fix(page): improve secure local resource access
2026-04-18 12:14:04 +05:30
Ejaaz Khan
13480db3fd
Merge pull request #36792 from aerele/fix/doctype-duplicate-auto-repeat
fix(doctype): disable allow_auto_repeat during duplication
2026-04-18 08:43:15 +05:30
Ejaaz Khan
031e032252
Merge pull request #38695 from UmakanthKaspa/fix/no-tag-filter
fix: no tags filter shows empty list
2026-04-18 08:38:30 +05:30
Ejaaz Khan
fe0e46b37c
Merge pull request #38689 from iamejaaz/ui-ux-improvement
feat: toggle awesomebar
2026-04-17 22:30:36 +05:30
UmakanthKaspa
5c3e6e6275 fix: no tags filter shows empty list 2026-04-17 19:55:05 +05:30
Ejaaz Khan
75a7267835 refactor: change help text shortcut 2026-04-17 18:02:04 +05:30
Ankush Menat
a96482b7b0
fix(DX): Allow db.commit from drop-down console (#38688)
This is anyways allowed, it's just extra friction at this point.

After using it for a while I feel we should allow it from drop-down
console too now.

It's risky, but hey, you're literally executing arbitrary code you just
wrote so I am trusting you.
2026-04-17 12:29:48 +00:00
Ejaaz Khan
002d58c53f feat: toggle awesomebar 2026-04-17 17:52:58 +05:30
Aarol D'Souza
c6d1a2362d
Merge pull request #38529 from AarDG10/fix-note
fix(note): force sanitization in notes
2026-04-17 17:00:49 +05:30
Shrihari Mahabal
181d01b88a
Merge pull request #38681 from ShrihariMahabal/print-pdf-text-escape
fix: escape text and long text fields when printing
2026-04-17 16:51:53 +05:30
Shrihari Mahabal
117c09e8d9 fix: escape text and long text fields when printing 2026-04-17 16:32:10 +05:30
diptanilsaha
37b05961c7
fix(security_settings): enabled track_changes and convert expires to UTC timezone (#38675)
* fix(security_settings): convert expires timestamp from system timezone to UTC

* fix(security_settings): enabled `track_changes` on `Security Settings` DocType
2026-04-17 14:29:28 +05:30
Soham Kulkarni
3418b221da
Merge pull request #38669 from sokumon/login-template 2026-04-17 13:07:49 +05:30
Ejaaz Khan
1280e3281d
Merge pull request #36606 from barredterra/web-hero
fix: move hero block inside content block
2026-04-17 12:53:02 +05:30
Ejaaz Khan
3359f8c41a
chore: update pypdf (#38670) 2026-04-17 12:50:47 +05:30
Dharanidharan2813
d6b5941c83 fix(doctype): disable allow_auto_repeat during duplication to prevent save failure 2026-04-17 12:44:03 +05:30
Shllokkk
44b5228598
feat: introduce standard and letter_head_for fields in letter head doctype (#38417)
* feat: introduce standard and letter_head_for fields in letter head doctype

* feat: introduce a module link field to letterhead doctype to support json creation

* feat: make Letter Head importable via sync

* test(Letter Head): fix the test_auto_image test case for letter head doctype

* fix: make module field depend on standard field value

* feat: introduce letter heads for standard reports

* fix: letter heads for non-standard reports

* fix: letter_head validation in report and letter head doctype edit access based on users

* fix: correct validation for standard letter head creation
2026-04-17 12:34:33 +05:30
sokumon
2df8959596 chore: update pypdf 2026-04-17 12:33:59 +05:30
mergify[bot]
6480613103
Merge branch 'develop' into web-hero 2026-04-17 06:55:08 +00:00
sokumon
7675fa782d fix(login): only show navbar when language picker is enabled 2026-04-17 12:18:50 +05:30
Ejaaz Khan
d6daefb3a3
Merge pull request #37736 from KerollesFathy/fix/date-picker-infinite-loop
fix: suppress change event during programmatic date set
2026-04-17 12:00:21 +05:30
Ejaaz Khan
05919d5d47
Merge pull request #37054 from aerele/feat/sidebar-hover-based-submenu
feat: add hover functionality for nested submenus in context menu
2026-04-17 11:33:16 +05:30
Ejaaz Khan
1b96aeaafb
chore: update stale closing date (#38666) 2026-04-17 10:18:46 +05:30
Ejaaz Khan
3040c4aa37
chore: update stale closing date 2026-04-17 10:09:05 +05:30
MochaMind
de4c53818a
fix: sync translations from crowdin (#38656)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: German translations

* fix: Hungarian translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Russian translations

* fix: Slovenian translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Burmese translations

* fix: Norwegian Bokmal translations
2026-04-17 10:06:35 +05:30
Ejaaz Khan
8f4c8baabb
Merge pull request #38636 from KerollesFathy/translate-fraction-currency
fix: add translation context for fraction currency
2026-04-17 10:00:54 +05:30
Ejaaz Khan
911c5bed2a
Merge pull request #38657 from KerollesFathy/validate-private-custom-html-block
fix(CustomHTMLBlock): validate private field in server-side
2026-04-17 09:59:55 +05:30
Ejaaz Khan
93274a6ec5
Merge pull request #38653 from kaulith/fix/notification-badge-on-sidebar
fix(ui): properly align notification indicator on sidebar badge
2026-04-17 09:58:52 +05:30
Raffael Meyer
bab7a830df
chore: update translations (#38662) 2026-04-16 23:20:58 +00:00
Kaushal Shriwas
1f5632fedb fix: fade notification indicator on sidebar hover 2026-04-17 00:43:19 +05:30
Kaushal Shriwas
7572e6fe45 feat: add aria-label to sidebar notification count 2026-04-17 00:22:33 +05:30
Kaushal Shriwas
5f85777c8e refactor: source notification unread count from boot 2026-04-16 23:51:58 +05:30
Kaushal Shriwas
e878ab1d7d feat: show unread notification count in sidebar 2026-04-16 23:41:34 +05:30
KerollesFathy
189afcf08b fix(CustomHTMLBlock): validate private field in server-side 2026-04-16 17:32:17 +00:00
Nikhil Kothari
d8ad02d643
fix: add "%d-%b-%y" to guess date format (#38655) 2026-04-16 16:42:09 +00:00
MochaMind
075fc61cd8
fix: sync translations from crowdin (#38632) 2026-04-16 15:33:23 +02:00
Kaushal Shriwas
f6dd823a25 fix(ui): enlarge and reposition sidebar notification indicator 2026-04-16 18:50:59 +05:30
Kaushal Shriwas
44ecd8b677 fix(ui): properly align notification indicator on sidebar badge 2026-04-16 18:44:49 +05:30
Raffael Meyer
b380f5ad8f
chore: update translations (#38652) 2026-04-16 13:12:10 +00:00
Aarol D'Souza
9f63f8167d
Merge pull request #38219 from kaulith/fix/prepared-report-timestamp-mismatch
fix: reload Prepared Report before save to avoid TimestampMismatchError
2026-04-16 17:19:02 +05:30
rohitwaghchaure
6bf4712930
Merge pull request #38631 from rohitwaghchaure/fixed-slow-query
fix: site slow having huge numbers of files
2026-04-16 16:04:44 +05:30
Soham Kulkarni
183c82b5a4
Merge pull request #38644 from sokumon/perf 2026-04-16 15:19:09 +05:30
sokumon
beb06e2a19 perf: setup routes only once 2026-04-16 14:39:53 +05:30
Soham Kulkarni
47da523d6c
Merge pull request #38641 from sokumon/center-breadcrumb 2026-04-16 14:15:47 +05:30
Soham Kulkarni
8f5254b227
Merge pull request #38642 from sokumon/make-ci-green-again 2026-04-16 14:11:11 +05:30
AarDG10
0c3cef5237 fix(page): improve secure local resource access 2026-04-16 13:57:49 +05:30
sokumon
538618e327 chore: update pyPDF 2026-04-16 12:59:11 +05:30
sokumon
8be028805c fix(minor): center breadcrumb 2026-04-16 12:50:49 +05:30
Ejaaz Khan
02896ed602
Merge pull request #38637 from iamejaaz/report-sticky-ui
feat: report sticky column from UI
2026-04-16 11:14:22 +05:30
Ejaaz Khan
a7484851fc
Merge branch 'develop' into report-sticky-ui 2026-04-16 10:31:29 +05:30
Ejaaz Khan
9febd8057c feat: report sticky column from UI 2026-04-16 10:29:40 +05:30
KerollesFathy
6fe3468dd0 fix: add translation context for fraction currency 2026-04-15 21:54:21 +00:00
Raffael Meyer
3896f18f56
chore: update translations (#38633) 2026-04-15 18:24:10 +00:00
Rohit Waghchaure
13bf909b22 fix: site slow having huge numbers of files 2026-04-15 19:59:16 +05:30
Raffael Meyer
9169e53278
chore: update translations (#38630) 2026-04-15 13:35:12 +00:00
Shariq Ansari
304283c222
Merge pull request #38588 from shariquerik/reset-password-fix 2026-04-15 16:13:28 +05:30
shariquerik
71613d6fc8 fix: correct wording in password reset message for consistency 2026-04-15 15:32:03 +05:30
Shariq Ansari
667787cb47
fix: updated message 2026-04-15 02:53:07 -07:00
Shariq Ansari
74a5b3c8a3
fix: updated message 2026-04-15 02:39:17 -07:00
shariquerik
33077e0a2c test: ensure consistent response and messaging in password reset functionality 2026-04-15 12:13:19 +05:30
shariquerik
fe1edb3c01 fix: change return type to None 2026-04-15 12:04:53 +05:30
shariquerik
9f92e7bf0d fix: enhance error handling in password reset process 2026-04-15 12:02:20 +05:30
Ejaaz Khan
afe9d9fd8d
Merge pull request #38498 from kaulith/fix/notification-badge-on-desktop
fix: show notification badge on desktop bell icon
2026-04-15 11:24:53 +05:30
Shrihari Mahabal
16440d71e9
Merge pull request #37861 from ShrihariMahabal/get-docs
feat: get_docs to fetch instantiated document objects from db
2026-04-15 11:24:21 +05:30
Ejaaz Khan
d86f6f6099
Merge pull request #38617 from imgullu786/fix/list-view-child-table
fix: use proper list view validation in customize form
2026-04-15 11:16:12 +05:30
Prathamesh Kurunkar
07ac80062f
Merge pull request #38356 from prathameshkurunkar7/improve-frappe-client-post-api-method
feat(frappe-client): enhance post_api method to send payloads in data/json
2026-04-15 11:02:26 +05:30
Sabu Siyad
9997b6c62e
fix(security-settings): newline at end and utc (#38613)
* fix(security-settings): use time in UTC

* fix(security_settings): `security.txt`: newline at the end
2026-04-15 09:54:32 +05:30
Md Gulam Gaush
fd94dc5333 fix: use proper list view validation in customize form 2026-04-15 08:33:11 +05:30
Raffael Meyer
573cff80b9
chore: update translations (#38614) 2026-04-15 03:46:40 +02:00
MochaMind
2dd654d8e3
fix: sync translations from crowdin (#38610)
* fix: Swedish translations

* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: German translations

* fix: Hungarian translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Russian translations

* fix: Slovenian translations

* fix: Serbian (Cyrillic) translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Croatian translations

* fix: Burmese translations

* fix: Bosnian translations

* fix: Norwegian Bokmal translations

* fix: Serbian (Latin) translations

* fix: Esperanto translations
2026-04-14 22:49:18 +05:30
Kerolles Fathy
4944dec916
Merge pull request #38548 from KerollesFathy/fix-int-float-dirty-on-input
fix(ui): mark form "Not Saved" on input for Int and Float fields
2026-04-14 22:48:04 +05:30
Ejaaz Khan
fa53a971c2
Merge pull request #38519 from kaulith/feat/link-field-configurable-clear-button
feat: restore clear button in Link fields with system setting configu…
2026-04-14 22:46:06 +05:30
Ejaaz Khan
93bec95024
Merge pull request #36615 from GursheenK/virtual-df-value-in-document-getter
fix: title for link field in virtual docfield titles
2026-04-14 19:51:34 +05:30
Ejaaz Khan
435f82a0f4
Merge pull request #37662 from safwansamsudeen/improve-barcode
fix: render barcodes in print view
2026-04-14 19:47:00 +05:30
Kerolles Fathy
8509c79877
fix(report): enhance the visibility of serial-no at first column (#38249)
* fix(report): enhance the visibility of serial-no at first column

* Revert "fix(report): enhance the visibility of serial-no at first column"

This reverts commit c2c7253b107707092d7e48541dab6964db5a0b3b.

* fix(report): remove padding from serial-no col
2026-04-14 19:36:47 +05:30
Ejaaz Khan
9593058f53
Merge pull request #38424 from KerollesFathy/add-shortcut-for-toggle-sidebar
feat: add shortcut for toggle sidebar
2026-04-14 19:33:29 +05:30
Ejaaz Khan
8e4d827d57
Merge pull request #38582 from ruthra-kumar/prevent_virutal_on_standard_fields
fix(customize form): prevent setting standard fields as virtual
2026-04-14 19:29:12 +05:30
Kaushal Shriwas
ff36f5dfc5 fix: show workspace sidebar with no module to all users 2026-04-14 18:54:16 +05:30
shariquerik
a0f4526c58 fix: update password reset tests for improved accuracy and messaging 2026-04-14 17:44:31 +05:30
Soham Kulkarni
c039f12008
Merge pull request #38595 from sokumon/make-ci-green-again 2026-04-14 17:40:09 +05:30
Sabu Siyad
ec9a60172f
feat: security.txt (#38530)
* feat: `security.txt`

* fix(security-settings): public_policy must start be https

* feat(security-settings): preview `security.txt`

* refactor(security-settings): security_txt logic

* feat(security-settings): security_txt expires

* refactor(security-txt): get content from security settings

* fix(security-txt): serve only over https

* fix(security-settings): change labels (plural)

- contacts
- languages

* refactor(security-settings): move to website module

* feat(security-settings): banner/alert on security.txt with link to RFC

* feat(security-txt): expiry alert emails

* fix(security-settings): banner gets duplicated on save

* refactor(security-settings): move to `Core` module

* test(security-settings): add unit tests

* fix(security-settings): translatable strings on throw
2026-04-14 17:22:22 +05:30
Aarol D'Souza
4e52cbfb95
Merge pull request #38566 from AarDG10/fix-user
fix(user): sanitize all html tags in name fields in User Doctype
2026-04-14 17:07:48 +05:30
Soham Kulkarni
7848c594c0
Merge pull request #38594 from sokumon/show-password-icon 2026-04-14 16:55:34 +05:30
Kaushal Shriwas
8cfa6a2c95 fix: use correct sysdefault key for link field clear button 2026-04-14 16:52:19 +05:30
Kaushal Shriwas
5a8156aaee feat: restore clear button in Link fields with system setting configuration 2026-04-14 16:52:11 +05:30
sokumon
0dc5fb490f chore: update Pillow 2026-04-14 16:47:24 +05:30
sokumon
bc4d742129 fix: use lucide icon for password control 2026-04-14 16:37:20 +05:30
Aarol D'Souza
c61766cd47
Merge pull request #38583 from AarDG10/fix-report-export
fix(reportview): support dict. when parsing fields
2026-04-14 16:25:23 +05:30
Soham Kulkarni
1d03647559
Merge pull request #38556 from nextchamp-saqib/remove-collapse-button 2026-04-14 16:23:50 +05:30
sokumon
ec1e203e4e fix: add a subtler color and use more informative cursors 2026-04-14 16:06:13 +05:30
Shariq Ansari
8764dada2a
Merge branch 'develop' into reset-password-fix 2026-04-14 15:28:01 +05:30
rohitwaghchaure
2b1e30384f
Merge pull request #38561 from rohitwaghchaure/fix-max_writes_per_transaction
feat: provision to configure max_writes_per_transaction in site config
2026-04-14 15:26:00 +05:30
shariquerik
f00c4b7738 fix: enhance password reset flow to prevent username enumeration 2026-04-14 15:23:04 +05:30
Hussain Nagaria
0259c373ff
Merge pull request #38418 from gajjug004/fix/link-fields-title-report-view 2026-04-14 14:27:46 +05:30
AarDG10
e334e327fb fix(reportview): support dict. when parsing fields
QB generates a dict. so added support for that when exporting into Excel/CSV
2026-04-14 12:48:31 +05:30
dependabot[bot]
949016c749
chore(deps): bump pypdf from 6.9.2 to 6.10.0 (#38534)
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.9.2 to 6.10.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.9.2...6.10.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.10.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 11:24:55 +05:30
ruthra kumar
d443a80ad4 fix(customize form): prevent setting standard fields as virtual 2026-04-14 11:24:43 +05:30
dependabot[bot]
1208521859
build(deps): bump lodash-es from 4.17.23 to 4.18.1 (#38383)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 11:19:11 +05:30
dependabot[bot]
fdc5f0f5cc
build(deps): bump codecov/codecov-action from 5 to 6 (#38344)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 11:13:04 +05:30
Saqib Ansari
18d73d8045 fix: tests 2026-04-14 11:07:48 +05:30
MochaMind
a01cadcd44
chore: update POT file (#38546) 2026-04-14 11:01:38 +05:30
Ejaaz Khan
2f05b7b368
Merge pull request #38577 from gajjug004/fix/remove-wildcard-hint-link-search
fix: remove misleading wildcard hint from link field advanced search
2026-04-14 11:01:12 +05:30
gajjug004
659bff98ad fix: remove misleading wildcard hint from link field advanced search 2026-04-14 10:41:11 +05:30
Soham Kulkarni
e780b0b509
Merge pull request #38573 from sokumon/priv-workspaces 2026-04-14 05:07:53 +05:30
sokumon
cca94708b0 fix: allow desktop icon renaming for non-standard icons 2026-04-14 04:51:10 +05:30
Soham Kulkarni
c938be1880
Merge pull request #38571 from sokumon/priv-workspaces 2026-04-14 01:41:24 +05:30
sokumon
1bf32d2cb1 fix: set for user while creating private workspace 2026-04-14 01:26:54 +05:30
Sumit Jain
f5fd66c1b4 chore: Remove rows_threshold_for_grid_search from DocType configuration 2026-04-13 22:11:21 +05:30
Sumit Jain
824d53cae0 feat: Implement bulk edit functionality in grid component 2026-04-13 22:04:32 +05:30
Sumit Jain
35c25ceae3 feat: Add allow_bulk_edit field to DocType configuration and model 2026-04-13 22:03:57 +05:30
AarDG10
a1d7fb77e3 fix(user): sanitize all html tags in name fields
Name fields shouldn't really be allowing HTML tags in User Doctype.
2026-04-13 20:56:47 +05:30
AarDG10
c3d8214124 feat(html_utils): introduce wildcard in sanitize_html
Introduces a wildcard i.e. Disallows all HTML tags when used.
2026-04-13 20:53:04 +05:30
prathameshkurunkar7
7da10131c0 fix(frappe-client): simplify post_api method to use json params 2026-04-13 17:45:10 +05:30
Hussain Nagaria
876bf3a6b2
Merge pull request #38557 from gajjug004/fix/onboarding-shown-when-disabled 2026-04-13 17:00:36 +05:30
Rohit Waghchaure
683019f296 feat: provision to configure max_writes_per_transaction in site config 2026-04-13 16:31:05 +05:30
Pratik
f0ef9295bd fix: add get_dynamic_linked_docs & get_linked_docs utils 2026-04-13 15:37:01 +05:30
gajjug004
72369a329f fix: respect Enable Onboarding setting in sidebar onboarding panel 2026-04-13 15:09:00 +05:30
Saqib Ansari
d0df12c326 refactor: remove collapse button 2026-04-13 14:28:30 +05:30
Hussain Nagaria
b354a30aed
refactor: more readable conditional 2026-04-13 12:56:25 +05:30
Ejaaz Khan
cdb24afaa4
Merge pull request #38539 from kaulith/fix/pretty-date-calendar-day-diff
fix(ui): use calendar days for relative timestamp display
2026-04-13 12:28:47 +05:30
Ejaaz Khan
c6b0587f3b
Merge pull request #38550 from AarDG10/fix-pdf
fix(print_utils): fix pdf rendering via chrome by considering bytes
2026-04-13 12:23:32 +05:30
Rucha Mahabal
cc74712304
feat: after_build hook (#38518)
* feat: `after_build` hook

* feat: add option to skip running `after_build` hooks

* feat(boilerplate): add `after_build` hook

* revert: "feat: add option to skip running `after_build` hooks"

This reverts commit 6e9d2c6a2333d487fcf4d1908c366b496a8d80b1.
Removing the flag for now as other hooks (like after/before migrate) don't have a skip option either
2026-04-13 12:15:38 +05:30
Aarol D'Souza
620c44863c
fix: correct regex in sidebar module app filtering (#38131)
Co-authored-by: petnd <58605206+petnd@users.noreply.github.com>
2026-04-13 11:53:47 +05:30
AarDG10
255a3e94fa fix(print_utils): fix pdf rendering via chrome by considering bytes
Issue has been caught w/ chrome pdf generator, it returns bytes. This fixes that by considering bytes and then turning it into a PdfWriter obj.
2026-04-13 11:53:39 +05:30
Ejaaz Khan
11a9eba342
Merge pull request #38533 from KerollesFathy/feat/frappe-confirm-custom-labels
feat: Add `primary_label` and `secondary_label` params to frappe.confirm
2026-04-13 11:32:07 +05:30
Kaushal Shriwas
44da9da9f7 fix(ui): use Math.floor instead of Math.round for day_diff 2026-04-12 14:23:00 +05:30
Raffael Meyer
2ac1998000
feat(File): add helper to copy attachment to different doc (#37972) 2026-04-11 21:45:47 +02:00
MochaMind
5fdebb67bf
fix: sync translations from crowdin (#38537) 2026-04-11 20:58:56 +02:00
Kaushal Shriwas
8aae2c921b fix(ui): use calendar days for relative timestamp display 2026-04-11 23:26:59 +05:30
Kaushal Shriwas
334d4d971f fix: validate hidden and mandatory fields without default in web form 2026-04-11 16:57:02 +05:30
KerollesFathy
4184f87703 feat: add primary_label and secondary_label params to frappe.confirm 2026-04-10 21:19:04 +00:00
Saqib Ansari
76eb3297cd refactor: set Retry-After header directly 2026-04-10 22:43:45 +05:30
Saqib Ansari
2f30dac5d8 feat: implement concurrency limiting decorator 2026-04-10 22:22:23 +05:30
AarDG10
10553f80ef fix(note): force sanitization in notes 2026-04-10 16:55:25 +05:30
Kaushal Shriwas
a80feaf8d4 fix: use indicator class for notification dot and sync seen state 2026-04-10 13:39:40 +05:30
Sumit Jain
9995bee63c feat: Enhance Data Exporter and MultiCheck UI with warning titles and tooltips 2026-04-10 12:16:21 +05:30
Ejaaz Khan
1b81ff8490
Merge pull request #38499 from frappe/l10n_develop
fix: sync translations from crowdin
2026-04-10 12:12:46 +05:30
Kaushal Shriwas
fa7b946bf8 fix: use bell-dot icon and scoped selectors for notification indicator 2026-04-10 09:35:31 +05:30
Ejaaz Khan
d14ac27e32
Merge pull request #38392 from Shllokkk/report-printing-fixes
fix: report printing fixes
2026-04-09 21:40:36 +05:30
MochaMind
dc86ee6e2b fix: Bosnian translations 2026-04-09 20:53:54 +05:30
MochaMind
f6b8a92b91 fix: Croatian translations 2026-04-09 20:53:51 +05:30
Shllokkk
820bc52201 fix: minor bugs in the print settings dialog and populate default print format field for a report 2026-04-09 19:22:52 +05:30
Shllokkk
45c04a85ac feat(report): add default_print_format link field to report doctype 2026-04-09 19:22:52 +05:30
Saqib Ansari
58618cd0f9
fix: add secondary action for amended documents with tooltip (#38510) 2026-04-09 15:30:03 +05:30
Ejaaz Khan
4b53fa7409
Merge pull request #38425 from krishna-254/fix-calendar-end-date-issue
fix: adjust end date for all-day events in calendar
2026-04-09 14:41:59 +05:30
Ejaaz Khan
525b7575b0
Merge pull request #38469 from KerollesFathy/fix/not-in-filter-null-values
fix: remove null values from "not in" filter
2026-04-09 14:13:18 +05:30
Ejaaz Khan
4d1aaf5932
Merge pull request #38506 from safwansamsudeen/fix-file-attachments-link
fix: attachments file link incorrect
2026-04-09 14:08:51 +05:30
Ejaaz Khan
ff38ee1763
Merge pull request #38503 from nextchamp-saqib/fix-allow-importing-custom-docperms
fix: allow importing custom docperms
2026-04-09 14:08:30 +05:30
Safwan Samsudeen
9690ab10bd fix: attachments file link incorrect 2026-04-09 13:05:14 +05:30
Aarol D'Souza
0122b49ef6
Merge pull request #37554 from AarDG10/refactor-password
refactor(user): misc. fixes and refactors
2026-04-09 11:26:33 +05:30
Saqib Ansari
d990bece32 fix: allow importing custom docperms 2026-04-09 11:14:49 +05:30
Kaushal Shriwas
f77e9f09f2 chore: resolve merge conflict with develop 2026-04-09 10:58:08 +05:30
Kaushal Shriwas
7111e942f2 fix: notification badge not clearing on click and missing on sidebar 2026-04-09 10:49:10 +05:30
Ankush Menat
9c77848b81 refactor: Simpler iterator implementation using itertools 2026-04-09 10:02:57 +05:30
Shrihari Mahabal
4c94239b1c
Merge pull request #38372 from ShrihariMahabal/load-notifications-on-demand
perf: load notifications and events on demand
2026-04-08 22:24:03 +05:30
Ankush Menat
02510e506a fix: get_docs - Always use iterator internally
When `get_docs` output is unknown, we might end up generating queries
for child table with `in (...)` containing thousands of doc names.

This doesn't fare well with databases, so it's better to chunk it to
1000 by default. This is an acceptable tradeoff IMO.
2026-04-08 21:59:44 +05:30
Ankush Menat
2364216fb1 fix: Avoid masking in get_docs
get_doc, so far doesn't do perm checks by default. Masking is part of
permissions.
2026-04-08 21:41:45 +05:30
Ankush Menat
b1a723f514 refactor: remove redundant lock_rows 2026-04-08 21:36:07 +05:30
Ankush Menat
8a0825fe6d test: don't hardcode throw-away doctype names 2026-04-08 21:31:04 +05:30
Ankush Menat
a303fbc3ea refactor: Consistent API for list/generator
Returning chunks is not expected API. Why? Because we should always be
able to do:

```python
for doc in frappe.get_docs(...):
    ...
```
2026-04-08 21:31:02 +05:30
Ankush Menat
0d833d658e refactor: use as_iterator instead of as_generator
Because it's already used in `db.sql`. So use consistent naming.
2026-04-08 21:17:25 +05:30
MochaMind
4a316e6842 fix: Swedish translations 2026-04-08 20:52:40 +05:30
Kaushal Shriwas
c5aa51a7e6 fix: show notification badge on desktop bell icon 2026-04-08 20:15:29 +05:30
Sumit Jain
041ab77e55 fix: hide Blank Template option if its exporting data 2026-04-08 17:58:24 +05:30
Sumit Jain
fe1019f7cc fix: show ID field only when its for export and in import if naming series is Set by User 2026-04-08 17:57:46 +05:30
Sumit Jain
ee32129517 feat: Add 'Include in Import Template' field to DocField configuration 2026-04-08 17:56:45 +05:30
mergify[bot]
ba254318a0
Merge branch 'develop' into fix-calendar-end-date-issue 2026-04-08 08:15:21 +00:00
Kaushal Shriwas
51cfc8181e
perf(query): replace Coalesce with OR IS NULL in func_in (#38336) 2026-04-08 10:57:07 +05:30
Ankush Menat
0ae52b051e test: use another doctype to avoid test pollution 2026-04-08 10:50:47 +05:30
Ankush Menat
0d8ddb5958 Merge branch 'develop' into get-docs 2026-04-08 10:50:31 +05:30
Soham Kulkarni
e6180c4ab4
Merge pull request #38487 from mikaeylinen/portal-no-cache 2026-04-08 10:39:21 +05:30
Aarol D'Souza
6841a4a808
Merge pull request #38481 from AarDG10/fix-junit-xml-output
fix(testing): use XMLTestRunner when junit_xml_output is wanted
2026-04-08 10:23:07 +05:30
Raffael Meyer
8a80840abd
fix(Email Account): create_dummy (#38480) 2026-04-07 22:37:09 +02:00
Mikael Ylinen
838506a6e1 fix(website): disable HTML caching for portal pages 2026-04-07 23:15:41 +03:00
Raffael Meyer
dadf822152
fix(Translation): don't remove HTML from source_text (#33558) 2026-04-07 21:09:56 +02:00
AarDG10
fe12722e4b fix(testing): use XMLTestRunner when junit_xml_output is wanted 2026-04-07 22:10:36 +05:30
Ejaaz Khan
62c297678d
Merge pull request #38450 from raizasafeel/feat/a11y
feat: add accessibillity to desk landing page icons
2026-04-07 21:22:35 +05:30
Ejaaz Khan
dc9aea5383
Merge pull request #38456 from iamejaaz/63835-number-card
fix(Dashboard): hide widget not working
2026-04-07 21:17:16 +05:30
raizasafeel
5748321526 feat: user menu made accessible 2026-04-07 15:39:25 +00:00
raizasafeel
c1ffb98693 feat: notifications button made accessible 2026-04-07 15:39:25 +00:00
raizasafeel
c0b12feb35 feat: desktop icons made accessible 2026-04-07 15:39:25 +00:00
raizasafeel
14ade113b3 feat: add focus to buttons and links 2026-04-07 15:39:25 +00:00
KerollesFathy
14edcce3b4 refactor: remove empty values after splitting 2026-04-07 15:34:47 +00:00
Ejaaz Khan
d6d732cf91 fix: hide widget not working 2026-04-07 15:34:17 +00:00
MochaMind
2748bb78bf
fix: German translations (#38467) 2026-04-07 21:00:34 +05:30
KerollesFathy
00cffe2881 fix(filter): remove empty values from value 2026-04-07 14:50:57 +00:00
Kaushal Shriwas
7957fa0902 fix: sync role_profile_name for user list view display 2026-04-07 19:53:55 +05:30
rohitwaghchaure
b80b121f51
Merge pull request #38461 from rohitwaghchaure/fixed-do_not_round_fields
feat: do not round fields
2026-04-07 17:24:16 +05:30
Rohit Waghchaure
1c47e262ae feat: do not round fields 2026-04-07 17:01:51 +05:30
Ankush Menat
b274d2ba11
Revert "perf: Add ignore_ifnull parameter to get_values call in version.py (#…" (#38458)
This reverts commit 2cb6c5edcc.
2026-04-07 10:54:23 +00:00
Nikhil Kothari
5fb76f30a6
fix(onboarding): hide onboarding card on mobile (#38453) 2026-04-07 09:43:15 +00:00
Krishna Pramod Shirsath
a2f2fb34b1
Merge branch 'frappe:develop' into fix-calendar-end-date-issue 2026-04-07 12:21:41 +05:30
Ankush Menat
2cb6c5edcc
perf: Add ignore_ifnull parameter to get_values call in version.py (#38442)
Co-authored-by: zeel prajapati <zeelprajapati321@gmail.com>
2026-04-07 03:46:57 +00:00
s-aga-r
5b4d36b087
fix(email): validate message size only when SIZE limit is greater than 0 (#38441) 2026-04-07 03:39:36 +00:00
Ejaaz Khan
8c3708a87a
Merge pull request #38342 from KerollesFathy/fix/link-autocomplete-description
fix: convert description to plain text after escaping
2026-04-06 23:39:47 +05:30
MochaMind
02c84e5682
fix: sync translations from crowdin (#38431)
* fix: Russian translations

* fix: Serbian (Cyrillic) translations

* fix: Swedish translations

* fix: Croatian translations

* fix: Bosnian translations

* fix: Serbian (Latin) translations
2026-04-06 22:12:05 +05:30
Kerolles Fathy
2aa234f907
Merge pull request #38432 from KerollesFathy/confirm-before-logout
fix(ux): add confirm before logout
2026-04-06 17:55:03 +02:00
diptanilsaha
6d12ae7a40
docs(schema): fixed SPECIAL_CHAR_PATTERN constant definition string (#38433) 2026-04-06 15:52:32 +00:00
KerollesFathy
842921c587 fix(ux): add confirm before logout
Note:
This not a new fix it's already merged before in #37880
But removed by mistake in this #38192 so this for make it come back :)
2026-04-06 15:18:15 +00:00
Shrihari Mahabal
381f5e5c06 fix: set notifications list to empty when no notifications 2026-04-06 18:50:31 +05:30
Shrihari Mahabal
2f0319d3b5
Merge pull request #38426 from ShrihariMahabal/xss-in-importer
fix(security): escape html in invalidation warnings in importer
2026-04-06 17:40:44 +05:30
Shrihari Mahabal
e562966d46 fix(security): escape html in invalidation warnings 2026-04-06 17:25:36 +05:30
Ejaaz Khan
961331131f
Merge pull request #38421 from KerollesFathy/fix/prev-and-next-doc-icon-arrow-direction
fix(ui): correct next/prev arrow direction in RTL
2026-04-06 16:47:35 +05:30
Krishna Shirsath
80d4c1f39d fix: adjust end date for all-day events in calendar 2026-04-06 16:35:45 +05:30
Soham Kulkarni
2ec2b7348f
Merge pull request #38422 from sokumon/comment-perms 2026-04-06 15:13:27 +05:30
KerollesFathy
2404bee1bb feat: add shortcut for toggle sidebar 2026-04-06 09:18:08 +00:00
sokumon
8d53f632e3 fix: remove unecessary user check for guest commenting 2026-04-06 13:53:43 +05:30
KerollesFathy
23f6b8c26d fix(ui): correct next/prev arrow direction in RTL 2026-04-06 07:56:53 +00:00
Aarol D'Souza
118cb4490f
Merge pull request #38215 from AarDG10/val-path
fix: validate path in render_include
2026-04-06 10:14:49 +05:30
AarDG10
b5ab941788 fix: validate path in render_include
Validate the parsed path in render_include by canonicalizing the path
2026-04-06 10:03:01 +05:30
Aarol D'Souza
3e25f6c878
Merge pull request #38378 from AarDG10/fix-api
fix: add perm check to get_values_for_link_and_dynamic_link_fields
2026-04-06 08:58:18 +05:30
gajjug004
697fa243c3 fix: inconsistent link title in report view 2026-04-06 08:03:59 +05:30
MochaMind
8c4191d957
fix: sync translations from crowdin (#38413) 2026-04-05 22:53:36 +02:00
MochaMind
6023638871
chore: update POT file (#38411) 2026-04-05 18:20:35 +05:30
Aarol D'Souza
8940902bb1
Merge pull request #38375 from AarDG10/revamp-bulk-update
fix(bulk_update): update conditions block in bulk_update to now only accept json
2026-04-05 09:16:40 +05:30
AarDG10
c24d0a5731 test: add test for the whitelisted bulk_update method
Added test for the whitelisted endpoint, in particular to test the parsing of the conditions.
2026-04-05 09:02:54 +05:30
MochaMind
0737691766
fix: sync translations from crowdin (#38401)
* fix: Swedish translations

* fix: Croatian translations

* fix: Bosnian translations
2026-04-04 21:49:33 +05:30
Ejaaz Khan
cf6ee3d90e
Merge pull request #38400 from iamejaaz/ui-pdf-debugging
fix: error when print templates are undefined
2026-04-04 18:06:45 +05:30
Ejaaz Khan
1f1c272e63 fix: error when print templates are undefined 2026-04-04 17:42:08 +05:30
Ejaaz Khan
f423bf4979
Merge pull request #38399 from iamejaaz/ui-pdf-debugging
feat(PrintFormat): UI pdf debugging
2026-04-04 17:29:30 +05:30
Ejaaz Khan
fcb40f71c4 fix: restricts PDF debug mode to developer mode only 2026-04-04 09:45:18 +05:30
Ejaaz Khan
849785b668 feat: add UI debugging option 2026-04-04 09:15:46 +05:30
Raffael Meyer
efe39fdce3
refactor: make RoleEditor child table fields configurable (#38394) 2026-04-03 21:46:24 +02:00
Nikhil Kothari
707f685154
fix: move onboarding widget when sidebar is collapsed (#38395)
* fix: move onboarding widget when sidebar is collapsed

* fix: add animation for collapse/expand
2026-04-03 18:00:05 +00:00
Ejaaz Khan
816a60cbee
Merge pull request #38381 from KerollesFathy/fix-package-readme
fix: check readme exists before writing to avoid TypeError on publish
2026-04-03 23:04:44 +05:30
barredterra
5876c70f86 fix: defer DocType exports until after save response
Defer standard **DocType** file export and controller generation until after the save response is sent.
This allows the client form to receive the updated document payload before dev-mode file writes trigger a web reload and prevents follow-up `TimestampMismatchError` on consecutive **DocType** saves without forcing a full page reload.
2026-04-03 16:20:38 +02:00
Soham Kulkarni
e39067bfcd
Merge pull request #38389 from sokumon/private-work 2026-04-03 16:25:09 +05:30
sokumon
57b3a224f4 fix: private workspaces routting
Co-authored-by: Rahul Agarwal <12agrawalrahul@gmail.com>
2026-04-03 15:02:55 +05:30
MochaMind
c9ea11189f
fix: sync translations from crowdin (#38384)
* fix: Serbian (Cyrillic) translations

* fix: Serbian (Latin) translations
2026-04-02 19:25:59 +02:00
Raffael Meyer
1df1537301
feat: make notification email customizable (#38365) 2026-04-02 19:24:45 +02:00
MochaMind
5e2687da21
fix: sync translations from crowdin (#38334)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: German translations

* fix: Hungarian translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Russian translations

* fix: Slovenian translations

* fix: Serbian (Cyrillic) translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Croatian translations

* fix: Burmese translations

* fix: Bosnian translations

* fix: Norwegian Bokmal translations

* fix: Serbian (Latin) translations

* fix: Esperanto translations

* fix: Hungarian translations

* fix: Russian translations
2026-04-02 12:55:16 +05:30
KerollesFathy
d62022f923 fix: check readme exists before writing to avoid TypeError on publish 2026-04-01 20:36:47 +00:00
Ejaaz Khan
205d025ae6
Merge pull request #38075 from wfhp/wfhp-fix-currency-keyboard
fix: use inputmode="decimal" for Float, Currency, and Percent fields
2026-04-01 21:17:41 +05:30
s-aga-r
b6c9cebc27
Merge pull request #37246 from prathameshkurunkar7/37186-inline_images-parameter-in-frappesendmail-is-ignored
fix(sendmail): respect inline_images parameter in sendmail
2026-04-01 18:10:30 +05:30
AarDG10
8d898a4ebc fix: add perm check to get_values_for_link_and_dynamic_link_fields 2026-04-01 17:32:35 +05:30
AarDG10
cbd1f8fe5c fix(bulk_update): update conditions block in bulk_update
Update conditions block to strictly use json. Conditions parsed will now have to be written in json instead of plain strings.
2026-04-01 13:51:17 +05:30
Ejaaz Khan
56d251527f
Merge pull request #38371 from gajjug004/fix/list-view-percent-progress-bar
fix: progress bar not rendering in list view for Percent fields
2026-04-01 00:04:19 +05:30
Shrihari Mahabal
73b757c201 chore: fix formatting 2026-03-31 22:59:03 +05:30
Shrihari Mahabal
ecc43b95b1 perf: load notifications and events on demand 2026-03-31 21:58:07 +05:30
gajjug004
2f2abbf0a3 fix: progress bar not rendering in list view for Percent fields 2026-03-31 21:17:21 +05:30
Ejaaz Khan
100d15a1f9
Merge pull request #38367 from nishkagosalia/gh-53962
fix: Autofocus on search bar of multiselect list
2026-03-31 18:03:42 +05:30
Nishka Gosalia
be3dcd2782 fix: Autofocus on search bar of multiselect list 2026-03-31 17:49:17 +05:30
Soham Kulkarni
983be399c1
Merge pull request #38363 from sokumon/home-icon 2026-03-31 17:02:02 +05:30
sokumon
7161c561f2 fix: change icon to home 2026-03-31 16:55:39 +05:30
prathameshkurunkar7
2d9b40a2db feat(frappe-client): enhance post_api method to send payloads in data/json 2026-03-31 14:37:24 +05:30
Ejaaz Khan
db4afbd021
Merge pull request #38355 from iamejaaz/3rd-attepmt-sticky-report
feat: sticky columns in report
2026-03-31 14:14:22 +05:30
Ejaaz Khan
3efd1f2899 feat: sticky columns in report 2026-03-31 13:47:15 +05:30
Shrihari Mahabal
271f179b00 refactor: remove unnecessary console log 2026-03-31 12:52:15 +05:30
sokumon
ed8f8766f1 feat: store last sidebar shown 2026-03-31 12:42:56 +05:30
Ejaaz Khan
0f6d4e59f0
Merge pull request #38313 from KerollesFathy/fix/show-options-description-on-currency-field
refactor: add options description for Currency fieldtype
2026-03-31 12:32:17 +05:30
Ejaaz Khan
a1eec24f78
Merge pull request #38347 from diptanilsaha/modal-list-item-head
fix(modal-list-item--head): `z-index` for modal `list-item--head`
2026-03-31 12:31:13 +05:30
Soham Kulkarni
bbd37f8d7b
Merge pull request #38346 from sokumon/filter-align 2026-03-31 12:25:20 +05:30
sokumon
f6e2d531e9 fix: filter alignment 2026-03-31 12:06:44 +05:30
diptanilsaha
48999a57f7 fix(modal-list-item--head): z-index for modal list-item--head 2026-03-31 12:06:21 +05:30
KerollesFathy
903538f6af refactor: enahnce variable names and description
Co-authored-by: Ejaaz Khan <ejaaz@frappe.io>
2026-03-30 17:57:12 +00:00
KerollesFathy
fdb8e14095 fix: convert description to plain text after escaping 2026-03-30 17:42:15 +00:00
Raffael Meyer
bedb08485e
fix: use secrets for random string generation (#38338) 2026-03-30 19:08:16 +02:00
Aarol D'Souza
0afbf2a98e
Merge pull request #38316 from AarDG10/feat-qb-gc
feat(custom): add separator support for Group_concat in Mariadb
2026-03-30 20:11:47 +05:30
Shrihari Mahabal
576bcfdefc
Merge pull request #38331 from ShrihariMahabal/complete-signup-xss
fix(security): escape 'key' parameter in complete signup
2026-03-30 16:35:33 +05:30
Shrihari Mahabal
a5e4ec654d fix(security): escape 'key' parameter in complete signup 2026-03-30 16:20:39 +05:30
Suraj Shetty
a62acb4956
Merge pull request #38135 from frappe/dependabot/npm_and_yarn/socket.io-parser-4.2.6 2026-03-30 15:25:32 +05:30
Suraj Shetty
1c5d51121e
Merge pull request #38257 from frappe/dependabot/npm_and_yarn/yaml-1.10.3 2026-03-30 15:25:14 +05:30
Suraj Shetty
6aa85f00e5
Merge pull request #38259 from frappe/dependabot/npm_and_yarn/picomatch-2.3.2 2026-03-30 15:25:01 +05:30
Suraj Shetty
2cef0fdefb
Merge pull request #38296 from frappe/dependabot/npm_and_yarn/brace-expansion-1.1.13 2026-03-30 15:24:26 +05:30
Suraj Shetty
fee0f3e3ea
Merge pull request #38312 from frappe/pot_develop_2026-03-29 2026-03-30 15:24:00 +05:30
Suraj Shetty
86daa79b99
Merge pull request #38317 from frappe/l10n_develop 2026-03-30 15:23:42 +05:30
Ankush Menat
5f1d4e9488
Merge pull request #38325 from frappe/ankush-patch-1
fix: Skip nulls in `client.get`
2026-03-30 12:22:27 +05:30
Ankush Menat
9e687317a7
fix: Skip nulls in client.get
This makes it consistent with `load.getdoc`
2026-03-30 12:08:28 +05:30
Soham Kulkarni
c9cb986b5b
Merge pull request #38288 from sokumon/make-ci-green-again 2026-03-30 11:31:46 +05:30
Ejaaz Khan
e00328715d
Merge pull request #38314 from frappe/remove_login_with_fc
Remove login with fc
2026-03-29 19:28:02 +05:30
MochaMind
a2dd411cdc fix: Italian translations 2026-03-29 18:54:53 +05:30
AarDG10
12e8995640 test: add and update group_concat test for mariadb
Updated test, since have decided to keep separator ',' as default for the query generation.
2026-03-29 18:03:21 +05:30
Bowrna
1833025b25 fix(login): Remove option to login with FC 2026-03-29 17:48:09 +05:30
AarDG10
8560db8bc1 feat(custom): add separator support for Group_concat in Mariadb 2026-03-29 17:45:46 +05:30
Bowrna
a9c19ddc89 fix(login): Remove option to login with FC 2026-03-29 17:26:43 +05:30
Bowrna
3e9be575cd fix(login): Remove option to login with FC 2026-03-29 17:11:01 +05:30
KerollesFathy
1941bb0a69 refactor: use lookup map for options field descriptions instead of if else 2026-03-29 10:45:35 +00:00
KerollesFathy
536f9d9180 fix: add options description for Currency fieldtype 2026-03-29 10:27:59 +00:00
frappe-pr-bot
d996a492e8 chore: update POT file 2026-03-29 09:44:42 +00:00
Ejaaz Khan
05489dbd2b
Merge pull request #38307 from frappe/fix-webform-upload
fix: add default if boot data isn't present
2026-03-29 00:15:12 +05:30
Ejaaz Khan
e2abc0e5b5
Merge pull request #38308 from iamejaaz/show-activity-system-setting
fix: show timeline for system setting
2026-03-29 00:02:47 +05:30
Ejaaz Khan
474cdcf975 style: fix whitespaces 2026-03-29 00:01:57 +05:30
Ejaaz Khan
aa0ded756a fix: show timeline for system setting 2026-03-28 23:54:23 +05:30
Safwan Samsudeen
b11ce887ba fix: add default if boot data isn't present 2026-03-28 23:54:18 +05:30
Ejaaz Khan
1049f5d6c2
Merge pull request #38306 from iamejaaz/sticky-header
fix(ListView): apply sticky globally
2026-03-28 23:28:32 +05:30
Ejaaz Khan
5dc3b6bf51 fix: apply sticky globally 2026-03-28 23:00:47 +05:30
Ejaaz Khan
95903450b5
Merge pull request #38268 from Shllokkk/report-printing-fixes
fix: allow standard print formats for reports
2026-03-28 19:58:37 +05:30
Ejaaz Khan
f924fed900
Merge pull request #38190 from AarDG10/fix-kanban
fix(kanban_view): fix routing when switching kanban board
2026-03-28 19:57:44 +05:30
Ejaaz Khan
9164d5acae
Merge pull request #38298 from kaulith/fix/portal-list-page-cache
fix: prevent portal list pages from being cached
2026-03-28 19:46:54 +05:30
Ejaaz Khan
0d78551e37
Merge pull request #38290 from frappe/change_fc_login_option
fix(login): Redirect to FC dashboard site page
2026-03-28 19:40:04 +05:30
Sagar Vora
44814c86ad
Merge pull request #38297 from sagarvora/fix-quick-entry-routing
feat: route back to document after creating link doc via Quick Entry
2026-03-28 18:07:51 +05:30
diptanilsaha
4901f64732
chore(language): enabled language pt-BR by default (#38301) 2026-03-28 11:54:53 +05:30
sokumon
8b3ff45780 chore: ignore pygaments vuln 2026-03-28 11:51:22 +05:30
Kaushal Shriwas
57854698ce fix: prevent portal list pages from being cached 2026-03-28 00:33:55 +05:30
Sagar Vora
59a280012c fix: route back to document after creating link doc via Quick Entry
Co-authored-by: Shankarv19bcr <95605398+Shankarv19bcr@users.noreply.github.com>
2026-03-28 00:30:28 +05:30
Shrihari Mahabal
d50f03fc82
Merge pull request #38207 from ShrihariMahabal/report-cache-update
fix: update user allowed reports cache after insert and trash
2026-03-27 21:53:02 +05:30
dependabot[bot]
eb1399f02d
build(deps): bump brace-expansion from 1.1.11 to 1.1.13
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:47:53 +00:00
Hussain Nagaria
ac3d5ee115
Merge pull request #38128 from kaulith/fix/qb-in-filter-none-handling 2026-03-27 18:12:11 +05:30
Shrihari Mahabal
359a0ca763
Merge pull request #38102 from ShrihariMahabal/remove-translations-from-boot
perf: remove translations from boot
2026-03-27 12:57:28 +05:30
Shrihari Mahabal
aee33c5bbe fix: add build version to translation version 2026-03-27 12:47:35 +05:30
Bowrna
207c9b4851 fix(login): Redirect to FC dashboard site page 2026-03-27 11:43:31 +05:30
Bowrna
c70e396e46 fix(login): Redirect to FC dashboard site page 2026-03-27 11:34:58 +05:30
sokumon
d1f6f8d753 fix: update requests 2026-03-27 11:13:34 +05:30
Soham Kulkarni
18c68b2905
Merge pull request #38286 from sokumon/filter-align 2026-03-27 11:09:34 +05:30
sokumon
81b2341689 fix(minor): center align sort and base filter 2026-03-27 10:55:18 +05:30
Soham Kulkarni
9c7606a660
Merge pull request #38280 from sokumon/mention-issue 2026-03-27 10:28:32 +05:30
Soham Kulkarni
dbdbba70f8
Merge pull request #38258 from frappe/dependabot/pip/pypdf-6.9.2 2026-03-27 10:24:54 +05:30
Ejaaz Khan
d0b033ba53
Merge pull request #38283 from iamejaaz/sticky-header
fix: make header sticky even when scrolling is disabled
2026-03-27 00:57:45 +05:30
Ejaaz Khan
dbcf8e1e7d fix: make header sticky even when scrolling is disabled 2026-03-27 00:56:15 +05:30
Ejaaz Khan
ada4df8f0f
Merge pull request #38282 from iamejaaz/sticky-header
feat: sticky header on list view
2026-03-27 00:32:14 +05:30
Shllokkk
f265da9caa fix: allow standard print formats for reports 2026-03-27 00:25:57 +05:30
Ejaaz Khan
21a2dd5057 fix: precommit errors of parenthesized 2026-03-27 00:15:31 +05:30
Ejaaz Khan
76aae9d525 feat: sticky header on list view 2026-03-27 00:10:57 +05:30
Ejaaz Khan
40577036ca
Merge pull request #38269 from AarDG10/fix-geo-field
fix(geo): display geolocation map in field
2026-03-26 22:56:58 +05:30
AarDG10
7d35f696e0 fix(geo): display geolocation map in field
Previously this was being hidden, this fixes that by displaying the map regardless as it should.
Co-authored-by: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com>
2026-03-26 20:18:23 +05:30
Shrihari Mahabal
79d9788a53 test: use sessions.get instead of internal get_bootinfo 2026-03-26 19:52:24 +05:30
sokumon
8e9446d630 fix: remove hover tooltip from mentions 2026-03-26 19:36:43 +05:30
Shrihari Mahabal
6d80ff3e4e test: fix tests 2026-03-26 18:35:13 +05:30
Sagar Vora
7be05130c7
refactor: remove repetitive use of frappe.get_hooks() (#38197)
* refactor: remove repetitive use of frappe.get_hooks()

* refactor: update variable name

---------

Co-authored-by: harsh patadia <harshpatadia4114@gmail.com>
2026-03-26 12:11:20 +00:00
Sagar Vora
f97388f5d9
Merge pull request #38270 from sagarvora/correct-flags 2026-03-26 17:35:05 +05:30
Sagar Vora
2a2350d3a4 test: ensure fields with accented chars are considered valid 2026-03-26 17:25:51 +05:30
Sagar Vora
17f9ca9819 fix: check for numeric arg first 2026-03-26 17:25:13 +05:30
Sagar Vora
0d415afdd5 fix: allow unicode chars in field regexes 2026-03-26 17:02:12 +05:30
Shrihari Mahabal
0b3e0f5c51 refactor: use random string hash for translation version 2026-03-26 15:41:43 +05:30
Jannat Patel
653ae1e47a
Merge pull request #38260 from pateljannat/login-as-first-user
fix: login as first user after setup wizard completes
2026-03-26 14:29:42 +05:30
Nikhil Kothari
9bf22368f2
feat(setup): better onboarding flow for Frappe Cloud (#36644)
* chore(setup): disable first session recording

* feat(setup): better onboarding flow for Frappe Cloud
2026-03-26 14:16:52 +05:30
Hussain Nagaria
11cc7740f0
Merge pull request #38232 from gajjug004/fix/link-control-ignore-user-permissions-fallback 2026-03-26 13:55:17 +05:30
Shrihari Mahabal
81eb7d6892 fix: increment translation version normally instead of incrby 2026-03-26 13:35:53 +05:30
Jannat Patel
de8cbf2d42 fix: login as first user after setup wizard completes 2026-03-26 13:25:55 +05:30
Shrihari Mahabal
1a03e5af8d fix: make translation version update for system and user translations both 2026-03-26 12:53:10 +05:30
Shrihari Mahabal
a574651acc refactor: add type hints to get_boot_translations 2026-03-26 12:53:09 +05:30
Shrihari Mahabal
9ef5aa256b perf: remove translations from boot 2026-03-26 12:53:09 +05:30
Kaushal Shriwas
c8ce8cdc23 test(query): remove manual commit from test 2026-03-26 12:45:05 +05:30
dependabot[bot]
e5a892cd59
build(deps): bump picomatch from 2.3.1 to 2.3.2
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 21:50:48 +00:00
dependabot[bot]
5347d4b49c
build(deps): bump pypdf from 6.9.1 to 6.9.2
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.9.1 to 6.9.2.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.9.1...6.9.2)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 20:51:55 +00:00
dependabot[bot]
484feb80f3
build(deps): bump yaml from 1.10.2 to 1.10.3
Bumps [yaml](https://github.com/eemeli/yaml) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 1.10.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 20:10:38 +00:00
MochaMind
e6c98b1edb
fix: sync translations from crowdin (#38246) 2026-03-25 17:36:06 +00:00
Raffael Meyer
174100b21c
ci: fix po review workflow (#38256) 2026-03-25 17:01:47 +00:00
Faris Ansari
f565ed2438
fix: apply exif orientation before stripping the tag (#37998) 2026-03-25 19:51:40 +05:30
Ankush Menat
6572760990 fix: Clear report cache for all users
Co-Authored-By: Shrihari Mahabal <shriharimahabal08@gmail.com>
2026-03-25 16:47:03 +05:30
Ankush Menat
fd4245fc56 refactor: trigger cache clearing with clear_cache hook 2026-03-25 16:45:22 +05:30
Ankush Menat
462b3f6419
Merge pull request #38245 from ShrihariMahabal/enqueue-cancellation-in-submission-queue
feat: Enqueue cancellation in submission queue
2026-03-25 16:40:22 +05:30
Shrihari Mahabal
057288d3b7 fix(ui): hide status banner after submission is complete 2026-03-25 16:00:28 +05:30
ruthra kumar
028fb1280c
Merge pull request #38235 from ruthra-kumar/better_btn_layout_in_report_footer
fix: better button layout in report footer in windowed view
2026-03-25 15:47:10 +05:30
Shrihari Mahabal
377cc70d8b feat: enqueue cancellation in submission queue 2026-03-25 15:27:20 +05:30
Ejaaz Khan
ba29bde4c8
Merge pull request #38237 from frappe/revert-37421-fix-filter-processing
Revert "fix: use `JSON.parse()` for filter processing"
2026-03-25 14:59:38 +05:30
Ejaaz Khan
6a4e810800
Revert "fix: use JSON.parse() for filter processing" 2026-03-25 14:41:36 +05:30
ruthra kumar
508e373edc fix: better button layout in report footer in windowed view 2026-03-25 14:01:46 +05:30
Soham Kulkarni
89fd385a60
Merge pull request #37973 from frappe/fix/misc-sidebar-editor 2026-03-25 13:51:51 +05:30
gajjug004
ba03f457bf fix(link): fallback to meta for ignore_user_permissions in Link control 2026-03-25 12:49:14 +05:30
Soham Kulkarni
d92362841a
Merge pull request #38022 from KerollesFathy/fix/config-for-side-item-url 2026-03-25 12:26:55 +05:30
Soham Kulkarni
9da1c63189
Merge pull request #38156 from sokumon/info-card-fixes 2026-03-25 12:26:18 +05:30
Kaushal Shriwas
c5164d0150 fix: reload Prepared Report before save to avoid TimestampMismatchError 2026-03-24 20:31:53 +05:30
Shrihari Mahabal
2efc3c9cb4 fix: update user allowed reports cache after insert and trash to reflect updated reports in dropdown 2026-03-24 15:02:12 +05:30
AarDG10
3c21467479 fix(kanban_view): clear route_options
Clears query params. in route. This resolves the issue where switching from 1 kanban view to another overwrites the filters present in the switched board resulting in a Not Saved status.
2026-03-23 22:09:30 +05:30
Kaushal Shriwas
6281eac44a test: add unit test 2026-03-20 12:27:54 +05:30
mergify[bot]
1907293ba7
Merge branch 'develop' into get-docs 2026-03-19 11:48:31 +00:00
sokumon
0606399780 fix: set minimum width for card 2026-03-19 17:00:03 +05:30
sokumon
1931693998 fix: various issues with info card 2026-03-19 16:55:46 +05:30
dependabot[bot]
448c517163
chore(deps): bump socket.io-parser from 4.2.4 to 4.2.6
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-18 18:48:24 +00:00
Kaushal Shriwas
19a6c5aa50 fix(query): handle none in IN filter value list via Coalesce 2026-03-18 16:57:49 +05:30
KerollesFathy
7e4b55977c fix(sidebar_item): make open URL behavior configurable
added an "Open in new tab" checkbox to sidebar item settings,
giving users direct control over how URLs are opened.

defaults to checked (_blank) to preserve existing behavior
2026-03-15 14:22:43 +00:00
KerollesFathy
94a50655a3 feat: add open in new tab checkbox field on ws sidebar item dt 2026-03-15 14:15:25 +00:00
Hussain Nagaria
777d663cd0 fix: reordering between / across section break fails
* since the indices become stale after reorder
* we need to splice instead of swap
2026-03-14 06:20:30 +05:30
Hussain Nagaria
7feb58a6b6 fix: save fails for sidebar with nested items 2026-03-13 06:56:49 +05:30
Hussain Nagaria
e1bb478303 fix(ui): styling of sidebar item edit controls 2026-03-13 06:54:38 +05:30
Hussain Nagaria
8590b25b19 fix: prevent horizontal scroll in edit mode 2026-03-13 06:38:31 +05:30
Hussain Nagaria
723ed06ffb fix: show empty section breaks in edit mode
* else how will we add the first child 😅
2026-03-13 06:29:38 +05:30
Hussain Nagaria
fbe4691d56 fix(ux): don't override Link To if non-empty 2026-03-13 06:23:56 +05:30
Shrihari Mahabal
7ff564c227 refactor: add support for distinct in get_docs 2026-03-10 14:00:08 +05:30
Shrihari Mahabal
c174881534 refactor: change existing functionality in framework to check if get_docs is working 2026-03-10 13:26:38 +05:30
AarDG10
3624bc6e43 test: rewrite test based on new changes
Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2026-03-09 17:08:19 +05:30
AarDG10
391fcdb1cb fix: strip sensitive content from being displayed in email queue
Strip sensitive info. like reset password link... from the email queue but retain crucial info. like email headers

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2026-03-09 16:42:23 +05:30
Shrihari Mahabal
16efc5fa45 chore: add docstring for get_docs 2026-03-09 12:39:56 +05:30
Shrihari Mahabal
e2fef24a08 test: add tests for get_docs 2026-03-09 12:31:08 +05:30
Shrihari Mahabal
1f96971622 feat: get_docs to get multiple instantiated document objects 2026-03-09 12:30:27 +05:30
AarDG10
6885bf8a64 refactor: return link only when used internally
Restrict _reset_password() for internal use. Return link when used as an internal func, whitelisted method to be used otherwise, when resetting password.

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2026-03-09 12:08:08 +05:30
KerollesFathy
b4d87d6140 fix(form_sidebar): copy document title 2026-03-08 17:35:02 +00:00
KerollesFathy
6bcaffa043 fix: suppress change event during programmatic date set
Fixes: #37715
2026-03-03 12:18:58 +00:00
WFHP
dd63ab9d9e
fix: use inputmode="decimal" for Float, Currency, and Percent fields
ControlFloat inherits from ControlInt, which sets `inputmode="numeric"`.
On mobile devices, this brings up a numeric keypad without a decimal point,
making it impossible to enter decimal values (e.g. 0.16, 0.18) for Float,
Currency, and Percent fields.

Fix: Override `input_mode` to `"decimal"` in ControlFloat. Per the HTML spec,
`inputmode="decimal"` instructs mobile browsers to display a numeric keypad
that includes a decimal separator. Since ControlCurrency and ControlPercent
both extend ControlFloat, they automatically inherit the fix.
2026-03-02 15:57:45 +08:00
AarDG10
503150f99f refactor(user): cleaner code in send_password_notification
Small refactor for cleaner code.

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2026-03-02 11:17:30 +05:30
Safwan Samsudeen
7e739faea7 fix: only render if barcode value is not an svg 2026-02-27 19:43:47 +05:30
Safwan Samsudeen
6e344db222 fix: support options
fix: bundle files
2026-02-27 18:38:58 +05:30
Safwan Samsudeen
97c3ce6408 fix: render barcodes in print view 2026-02-27 17:19:28 +05:30
AarDG10
fd40eef2d3 fix(user): send mail to user to indicate that their password has been updated
Send an e-mail to user to indicate that their password has been changed, fixes a security flaw where user would just be logged out and have no clue as to what occurred

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2026-02-26 10:55:34 +05:30
prathameshkurunkar7
90615ea4df docs(test_email_body): clarify test docs 2026-02-19 15:04:26 +05:30
prathameshkurunkar7
a252e7e265 fix(sendmail): respect inline_images parameter in sendmail 2026-02-19 14:53:32 +05:30
Praveenkumar26-S
dea2b7d81e feat: add hover functionality for nested submenus in context menu 2026-02-16 12:22:49 +05:30
barredterra
820c9092e9 fix: move hero block inside content block
The `hero` block was defined at the top level of `web.html`, but since `web.html` extends `base.html` which has no `hero` block, that content was simply discarded. By moving it inside the `content` block, child template's overrides will now work correctly.
2026-02-02 23:13:00 +01:00
Gursheen Anand
f1731981a8 fix: query filters breaking for title virtual fields 2026-02-02 22:16:21 +05:30
Gursheen Anand
36d471b98d fix: show title field values in link for virtual fields 2026-02-02 22:14:13 +05:30
Gursheen Anand
7e92362892 fix: don't return virtual values before save 2026-02-02 22:12:01 +05:30
Gursheen Anand
dbfa0495ab refactor: common util for fetching virtual field value 2026-02-02 18:07:52 +05:30
Gursheen Anand
4544310419 feat: evaluate virtual docfield value in get method 2026-02-02 17:39:30 +05:30
317 changed files with 190129 additions and 188119 deletions

119
.github/helper/ci.py vendored Normal file
View file

@ -0,0 +1,119 @@
"""
Script to run Python tests while capturing accurte coverage.
Enabling coverage after `frappe` is imported leaves out a lot of lines that are imported by
default.
This is essentially a copy of `frappe/coverage.py` BUT also triggers test runner with desired
configuration.
"""
import json
import sys
import os
from pathlib import Path
from coverage import Coverage
STANDARD_INCLUSIONS = ["*.py"]
STANDARD_EXCLUSIONS = [
"*.js",
"*.xml",
"*.pyc",
"*.css",
"*.less",
"*.scss",
"*.vue",
"*.html",
"*/test_*/*",
"*/node_modules/*",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
".github/*",
]
# tested via commands' test suite
TESTED_VIA_CLI = [
"*/frappe/installer.py",
"*/frappe/utils/install.py",
"*/frappe/utils/scheduler.py",
"*/frappe/utils/doctor.py",
"*/frappe/build.py",
"*/frappe/database/__init__.py",
"*/frappe/database/db_manager.py",
"*/frappe/database/**/setup_db.py",
]
FRAPPE_EXCLUSIONS = [
"*/tests/*",
"*/commands/*",
"*/frappe/change_log/*",
"*/frappe/exceptions*",
"*/frappe/desk/page/setup_wizard/setup_wizard.py",
"*/frappe/coverage.py",
"*frappe/setup.py",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
"*/frappe/database/postgres/*",
"*/.github/helper/ci.py",
"*/frappe/database/sqlite/*",
*TESTED_VIA_CLI,
]
def get_bench_path():
"""Get the path to the bench directory."""
return Path(__file__).resolve().parents[4]
class CodeCoverage:
"""
Context manager for handling code coverage.
This class sets up code coverage measurement for a specific app,
applying the appropriate inclusion and exclusion patterns.
"""
def __init__(self, with_coverage, app, outfile="coverage.xml"):
self.with_coverage = with_coverage
self.app = app or "frappe"
self.outfile = outfile
def __enter__(self):
if self.with_coverage:
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), "apps", self.app)
omit = STANDARD_EXCLUSIONS[:]
if self.app == "frappe":
omit.extend(FRAPPE_EXCLUSIONS)
self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
assert "frappe" not in sys.modules, "frappe already imported, coverage will be inaccurate"
self.coverage.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.with_coverage:
self.coverage.stop()
self.coverage.save()
self.coverage.xml_report(outfile=self.outfile)
print("Saved Coverage")
if __name__ == "__main__":
app = "frappe"
site = os.environ.get("SITE") or "test_site"
with_coverage = json.loads(os.environ.get("CAPTURE_COVERAGE", "true").lower())
# Parse build information from environment variables
build_number = int(os.environ.get("BUILD_NUMBER"))
total_builds = int(os.environ.get("TOTAL_BUILDS"))
# Run tests with code coverage
with CodeCoverage(with_coverage=with_coverage, app=app):
from frappe.parallel_test_runner import ParallelTestRunner
runner = ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds)
runner.setup_and_run()

2
.github/stale.yml vendored
View file

@ -5,7 +5,7 @@ daysUntilStale: 60
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 5
daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:

View file

@ -29,17 +29,10 @@ on:
enable-coverage:
required: false
type: boolean
default: false
default: true
jobs:
unit-test:
name: Unit
runs-on: ubuntu-latest
steps:
- id: placeholder
run: |
echo "Evolution towards a set of (fast) unit tests which run without a DB connection is being planned"
gen-idx-integration:
name: Gen Integration Test Matrix
runs-on: ubuntu-latest
@ -100,7 +93,6 @@ jobs:
python-version: ${{ inputs.python-version }}
node-version: ${{ inputs.node-version }}
disable-socketio: true
enable-coverage: ${{ inputs.enable-coverage }}
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
db: ${{ matrix.db }}
env:
@ -108,43 +100,22 @@ jobs:
- name: Run Tests
run: |
bench --site test_site \
run-parallel-tests \
--app "${{ github.event.repository.name }}" \
--total-builds ${{ inputs.parallel-runs }} \
--build-number ${{ matrix.index }} 2> >(tee -a stderr.log >&2)
cd sites && ../env/bin/python3 ../apps/frappe/.github/helper/ci.py
# Process warnings and create annotations
if [ -s stderr.log ] && [ "$DB" == "mariadb" ]; then
echo "Processing deprecation warnings..."
grep -E "DeprecationWarning" stderr.log | sort -u | while read -r warning; do
# Extract file path, line number, and warning type
file_info=$(echo "$warning" | grep -oP '^.*?:\d+:')
file_path=$(echo "$file_info" | cut -d':' -f1)
line_number=$(echo "$file_info" | cut -d':' -f2)
warning_type=$(echo "$warning" | grep -oP '\w+Warning')
# Extract the actual warning message
message=$(echo "$warning" | sed -E "s/^.*$warning_type: //")
# Create the annotation
echo "::warning file=${file_path},line=${line_number}::${warning_type}: ${message}"
done
else
echo "No deprecation warnings found."
fi
env:
DB: ${{ matrix.db }}
# consumed by bench run-parallel-tests
CAPTURE_COVERAGE: ${{ inputs.enable-coverage }}
BUILD_NUMBER: ${{ matrix.index }}
TOTAL_BUILDS: ${{ inputs.parallel-runs }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN || '' }}
- name: Upload coverage data
uses: actions/upload-artifact@v7
if: inputs.enable-coverage
if: ${{ inputs.fake-success == false && inputs.enable-coverage }}
with:
name: coverage-${{ matrix.db }}-${{ matrix.index }}
path: ./sites/*-coverage*.xml
path: ./sites/*coverage*.xml
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
@ -164,15 +135,14 @@ jobs:
# TIP: Use these for checks, e.g. Server / Tests / Success
success:
name: Success
needs: [unit-test, integration-test]
needs: [integration-test]
if: always()
runs-on: ubuntu-latest
steps:
- name: Unit '${{ needs.unit-test.result }}' / Integration '${{ needs.integration-test.result }}'
- name: Integration '${{ needs.integration-test.result }}'
shell: python
run: |
stati = [
'${{ needs.unit-test.result }}',
'${{ needs.integration-test.result }}',
]

59
.github/workflows/backport_reminder.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Backport Reminder
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
remind:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/github-script@v8
with:
script: |
const labelName = 'defer backport';
const marker = '<!-- backport-reminder -->';
const waitDays = 14;
const maxDays = 30;
const now = new Date();
const query = `is:pr is:merged label:"${labelName}" repo:${context.repo.owner}/${context.repo.repo}`;
const searchResult = await github.rest.search.issuesAndPullRequests({ q: query });
for (const pr of searchResult.data.items) {
const { data: fullPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
if (!fullPr.merged_at) continue;
const mergedAt = new Date(fullPr.merged_at);
const diffInDays = (now - mergedAt) / (1000 * 60 * 60 * 24);
if (diffInDays >= waitDays && diffInDays <= maxDays) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100
});
const alreadyReminded = comments.some(c => c.body.includes(marker));
if (!alreadyReminded) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `${marker}\n**Backport Reminder**: This PR was merged ${Math.floor(diffInDays)} days ago. Time to backport!`
});
}
}
}

View file

@ -95,7 +95,7 @@ jobs:
run: |
pip install pip-audit
cd ${GITHUB_WORKSPACE}
pip-audit --desc on --ignore-vuln PYSEC-2023-312 .
pip-audit --desc on --ignore-vuln PYSEC-2023-312 --ignore-vuln CVE-2026-4539 .
precommit:
name: 'Pre-Commit'

View file

@ -5,7 +5,6 @@ on:
types: [opened, reopened, synchronize, ready_for_review]
branches:
- develop
- "version-[0-9][0-9]-hotfix"
paths:
- "**/*.po"
@ -21,7 +20,7 @@ jobs:
permissions:
contents: read
issues: write
pull-requests: read
pull-requests: write
steps:
- name: Checkout

View file

@ -1,8 +1,6 @@
name: Server
on:
repository_dispatch:
types: [frappe-framework-change]
pull_request:
workflow_dispatch:
schedule:
@ -18,14 +16,9 @@ permissions:
contents: read
jobs:
typecheck:
name: Types
uses: ./.github/workflows/_base-type-check.yml
checkrun:
name: Plan Tests
runs-on: ubuntu-latest
needs: typecheck
outputs:
build: ${{ steps.check-build.outputs.build }}
run_postgres: ${{ steps.check-build.outputs.run_postgres }}
@ -48,7 +41,6 @@ jobs:
enable-postgres: ${{ needs.checkrun.outputs.run_postgres == 'true' }} # This enables PostgreSQL to run tests
enable-sqlite: false # This will test against both MariaDB and SQLite if enabled
parallel-runs: 2
enable-coverage: ${{ github.event_name != 'pull_request' }}
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
needs: checkrun
secrets: inherit
@ -67,37 +59,16 @@ jobs:
name: Coverage Wrap Up
needs: [test, checkrun]
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Clone
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v8.0.1
- name: Upload coverage data
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
name: Server
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
flags: server
dispatch:
name: Downstream
runs-on: "ubuntu-latest"
needs: [test, migrate]
if: ${{ contains( github.event.pull_request.labels.*.name, 'trigger-downstream-ci') }}
strategy:
matrix:
repo:
- frappe/erpnext
- frappe/lending
- frappe/hrms
steps:
- name: Dispatch Downstream CI (if supported)
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.CI_PAT }}
repository: ${{ matrix.repo }}
event-type: frappe-framework-change
client-payload: '{"frappe_sha": "${{ github.sha }}"}'

View file

@ -2,8 +2,6 @@ name: UI
on:
pull_request:
repository_dispatch:
types: [frappe-framework-change]
workflow_dispatch:
schedule:
# Run everday at midnight UTC / 5:30 IST
@ -44,35 +42,6 @@ jobs:
uses: ./.github/workflows/_base-ui-tests.yml
with:
parallel-runs: 3
enable-coverage: ${{ github.event_name != 'pull_request' }}
enable-coverage: false
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
needs: checkrun
coverage:
name: Coverage Wrap Up
needs: [test, checkrun]
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v8.0.1
- name: Upload python coverage data
uses: codecov/codecov-action@v5
with:
name: UIBackend
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
exclude: coverage-js*
flags: server-ui
- name: Upload JS coverage data
uses: codecov/codecov-action@v5
with:
name: Cypress
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
exclude: coverage-py*
verbose: true
flags: ui-tests

View file

@ -29,10 +29,6 @@ flags:
paths:
- "**/*.py"
carryforward: true
ui-tests:
paths:
- "**/*.js"
carryforward: true
server-ui:
paths:
- "**/*.py"

View file

@ -111,4 +111,76 @@ context("Grid", () => {
cy.get("@table-form").find(".grid-footer-toolbar").click();
});
});
it("shows edit button only when child table allow_bulk_edit is enabled", () => {
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.window()
.its("cur_frm")
.then((frm) => {
const grid = frm.get_field("phone_nos").grid;
grid.meta.allow_bulk_edit = false;
grid.refresh_edit_rows_button();
});
cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
cy.get("@table").find(".grid-edit-rows").should("have.class", "hidden");
cy.window()
.its("cur_frm")
.then((frm) => {
const grid = frm.get_field("phone_nos").grid;
grid.meta.allow_bulk_edit = true;
grid.refresh_edit_rows_button();
});
cy.get("@table").find(".grid-edit-rows").should("not.have.class", "hidden");
});
it("bulk edit updates only selected child rows", () => {
const updated_phone = `99999${Date.now().toString().slice(-5)}`;
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.window()
.its("cur_frm")
.then((frm) => {
const grid = frm.get_field("phone_nos").grid;
grid.meta.allow_bulk_edit = true;
grid.refresh_edit_rows_button();
expect(frm.doc.phone_nos.length).to.be.greaterThan(1);
const phone_df = grid.docfields.find((df) => df.fieldname === "phone");
expect(phone_df).to.exist;
cy.wrap(phone_df.label).as("phoneFieldLabel");
cy.wrap(frm.doc.phone_nos[1].phone || "").as("secondRowPhoneBefore");
});
cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
cy.get("@table").find(".grid-edit-rows").click({ force: true });
cy.window()
.its("cur_dialog")
.then((dialog) => {
cy.get("@phoneFieldLabel").then((phoneFieldLabel) => {
return dialog
.set_value("field", phoneFieldLabel)
.then(() => dialog.set_value("value", updated_phone))
.then(() => {
dialog.get_primary_btn().click();
});
});
});
cy.window().its("cur_frm.doc.phone_nos.0.phone").should("eq", updated_phone);
cy.window()
.its("cur_frm")
.then((frm) => {
cy.get("@secondRowPhoneBefore").then((secondRowPhoneBefore) => {
expect(frm.doc.phone_nos[1].phone || "").to.equal(secondRowPhoneBefore);
});
});
});
});

View file

@ -1573,6 +1573,7 @@ from frappe.config import get_common_site_config, get_conf, get_site_config
from frappe.core.doctype.system_settings.system_settings import get_system_settings
from frappe.model.document import (
get_doc,
get_docs,
get_lazy_doc,
copy_doc,
new_doc,
@ -1594,6 +1595,7 @@ from frappe.utils.error import log_error
from frappe.utils.formatters import format_value
from frappe.utils.print_utils import get_print, attach_print
from frappe.email import sendmail
from frappe.concurrency_limiter import concurrent_limit
# for backwards compatibility
format = format_value

View file

@ -97,7 +97,8 @@ def get_values_for_link_and_dynamic_link_fields(doc_dict):
doctype = field.options if field.fieldtype == "Link" else doc_dict.get(field.options)
link_doc = frappe.get_doc(doctype, doc_fieldvalue)
link_doc = frappe.get_doc(doctype, doc_fieldvalue, check_permission="read")
link_doc.apply_fieldlevel_read_permissions()
doc_dict.update({field.fieldname: link_doc})

View file

@ -126,6 +126,12 @@ def application(request: Request):
elif request.path.startswith("/private/files/"):
response = frappe.utils.response.download_private_file(request.path)
elif request.path == "/.well-known/security.txt" and request.method == "GET":
if request.scheme != "https":
raise NotFound
security_settings = frappe.get_doc("Security Settings")
response = Response(security_settings.security_txt, content_type="text/plain")
elif request.path.startswith("/.well-known/") and request.method == "GET":
response = handle_wellknown(request.path)

View file

@ -53,6 +53,8 @@ def get_apps():
def get_route(app_name):
if app_name not in frappe.get_installed_apps():
return "/apps" # Invalid defaults
apps = frappe.get_hooks("add_to_apps_screen", app_name=app_name)
app = next((app for app in apps if app.get("name") == app_name), None)
return app.get("route") if app and app.get("route") else "/apps"
@ -89,6 +91,9 @@ def get_default_path():
@frappe.whitelist()
def set_app_as_default(app_name: str):
if app_name not in frappe.get_installed_apps():
frappe.throw(_("App {} is not installed").format(frappe.bold(app_name)))
if frappe.db.get_value("User", frappe.session.user, "default_app") == app_name:
frappe.db.set_value("User", frappe.session.user, "default_app", "")
else:

View file

@ -155,7 +155,9 @@ class LoginManager:
self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password():
doc = frappe.get_doc("User", self.user)
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
frappe.local.response["redirect_to"] = doc._reset_password(
send_email=False, password_expired=True
)
frappe.local.response["message"] = "Password Reset"
return False
@ -724,9 +726,13 @@ def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=Non
raise frappe.AuthenticationError
doctype = frappe_authorization_source or "User"
try:
docname = frappe.db.get_value(
doctype=doctype, filters={"api_key": api_key, "enabled": True}, fieldname=["name"]
)
except Exception:
raise frappe.AuthenticationError
if not docname:
raise frappe.AuthenticationError
form_dict = frappe.local.form_dict

View file

@ -78,6 +78,9 @@ def get_bootinfo():
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
bootinfo.notification_unread_count = frappe.db.count(
"Notification Log", {"read": 0, "for_user": frappe.session.user}
)
bootinfo.onboarding_tours = get_onboarding_ui_tours()
set_time_zone(bootinfo)
@ -342,10 +345,10 @@ def get_user_pages_or_reports(parent, cache=False):
def load_translations(bootinfo):
from frappe.translate import get_messages_for_boot
from frappe.translate import get_translation_version
bootinfo["lang"] = frappe.lang
bootinfo["__messages"] = get_messages_for_boot()
bootinfo["translations_version"] = get_translation_version()
def get_user_info():
@ -562,8 +565,9 @@ def get_sidebar_items(allowed_workspaces):
sidebar_doc = sidebar
if (
frappe.session.user == "Administrator"
or sidebar_doc.module in sidebar_doc.user.allow_modules
or sidebar_title == "My Workspaces"
or not sidebar_doc.module
or sidebar_doc.module in sidebar_doc.user.allow_modules
):
sidebar_items[sidebar_title.lower()] = {
"label": sidebar_title,
@ -590,6 +594,7 @@ def get_sidebar_items(allowed_workspaces):
"filters": item.filters,
"route_options": item.route_options,
"tab": item.navigate_to_tab,
"open_in_new_tab": item.open_in_new_tab,
}
if item.link_type == "Report" and item.link_to and frappe.db.exists("Report", item.link_to):
report_type, ref_doctype = frappe.db.get_value(

View file

@ -114,7 +114,7 @@ def get(
doc.check_permission()
doc.apply_fieldlevel_read_permissions()
return doc.as_dict()
return doc.as_dict(no_nulls=True)
@frappe.whitelist()

View file

@ -144,7 +144,6 @@ def main(
verbosity=2 if testing_module_logger.getEffectiveLevel() < logging.INFO else 1,
tb_locals=testing_module_logger.getEffectiveLevel() <= logging.INFO,
cfg=test_config,
buffer=not debug, # unfortunate as it messes up stdout/stderr output order
)
if doctype or doctype_list_path:
@ -159,11 +158,14 @@ def main(
discover_all_tests(apps, runner)
results = []
global unittest_runner
for app, category, suite in runner.iterRun():
click.secho(
f"\nRunning {suite.countTestCases()} {category} tests for {app}", fg="cyan", bold=True
)
results.append([app, category, runner.run(suite)])
main_runner = unittest_runner if junit_xml_output and unittest_runner else runner
res = main_runner.run(suite)
results.append([app, category, res])
success = all(r.wasSuccessful() for _, _, r in results)
if not success:

View file

@ -108,6 +108,19 @@ def build(
print("Compiling translations for", app)
compile_translations(app, force=force)
run_after_build_hook(apps)
def run_after_build_hook(apps):
from importlib import import_module
for app in apps:
for fn in frappe.get_hooks("after_build", app_name=app):
modulename = ".".join(fn.split(".")[:-1])
methodname = fn.split(".")[-1]
method = getattr(import_module(modulename), methodname)
method()
@click.command("watch")
@click.option("--apps", help="Watch assets for specific apps")

View file

@ -0,0 +1,125 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
"""
Concurrency limiter for expensive whitelisted methods.
Provides a @frappe.concurrent_limit() decorator that limits the number of
simultaneous in-flight executions of a function across all gunicorn workers
using a Redis-backed semaphore (LIST + BLPOP).
Usage::
@frappe.whitelist(allow_guest=True)
@frappe.concurrent_limit(limit=3)
def download_pdf(...):
...
"""
from collections.abc import Callable
from functools import wraps
import frappe
from frappe.exceptions import ServiceUnavailableError
from frappe.utils import cint
from frappe.utils.caching import redis_cache
from frappe.utils.redis_semaphore import RedisSemaphore
# Default wait timeout (seconds) before returning 503 to the caller.
_DEFAULT_WAIT_TIMEOUT = 10
@redis_cache(shared=True)
def _default_limit() -> int:
"""Derive a sensible default concurrency limit from gunicorn's max concurrency."""
return max(1, gunicorn_max_concurrency() // 2)
def gunicorn_max_concurrency() -> int:
"""Detect max concurrent requests from the running gunicorn master's cmdline."""
import os
fallback = 4
try:
ppid = os.getppid()
with open(f"/proc/{ppid}/cmdline", "rb") as f:
args = f.read().rstrip(b"\0").decode().split("\0")
if not any("gunicorn" in a for a in args):
return fallback
workers = _extract_cli_int(args, "-w", "--workers") or fallback
threads = _extract_cli_int(args, "--threads") or 1
return workers * threads
except OSError:
return fallback
def _extract_cli_int(args: list[str], *flags: str) -> int | None:
"""Return the integer value for a CLI flag from a split argument list.
Handles both ``--flag value`` and ``--flag=value`` forms.
"""
for i, arg in enumerate(args):
for flag in flags:
if arg == flag and i + 1 < len(args):
return int(args[i + 1])
if arg.startswith(f"{flag}="):
return int(arg.split("=", 1)[1])
return None
def concurrent_limit(limit: int | None = None, wait_timeout: int = _DEFAULT_WAIT_TIMEOUT):
"""Decorator that limits simultaneous in-flight executions of the wrapped function.
:param limit: Maximum number of concurrent executions. Defaults to half of ``workers x threads``
as detected from the gunicorn master process.
:param wait_timeout: Seconds to wait for a free slot before returning 503.
Defaults to 10 s.
The limiter is skipped entirely for background jobs, CLI commands, and
tests that call functions directly (i.e. outside of an HTTP request).
"""
def decorator(fn: Callable) -> Callable:
@wraps(fn)
def wrapper(*args, **kwargs):
# Skip concurrency limiting outside of HTTP requests (background jobs,
# CLI commands, tests that call functions directly, etc.).
if getattr(frappe.local, "request", None) is None:
return fn(*args, **kwargs)
_limit = cint(limit) if limit is not None else _default_limit()
key = f"concurrency:{fn.__module__}.{fn.__qualname__}"
sem = RedisSemaphore(key, _limit, wait_timeout, shared=True)
token = sem.acquire()
if not token:
retry_after = max(1, int(wait_timeout))
if (headers := getattr(frappe.local, "response_headers", None)) is not None:
headers.set("Retry-After", str(retry_after))
exc = ServiceUnavailableError(frappe._("Server is busy. Please try again in a few seconds."))
exc.retry_after = retry_after
raise exc
try:
return fn(*args, **kwargs)
finally:
sem.release(token)
return wrapper
return decorator
@frappe.whitelist()
def get_stats() -> dict:
frappe.only_for("System Manager")
cached_limit = _default_limit()
gunicorn_limit = gunicorn_max_concurrency()
return {
"cached_limit": cached_limit,
"gunicorn_limit": gunicorn_limit,
}

View file

@ -129,7 +129,7 @@ def _accept_invitation(key: str, in_test: bool) -> None:
hashed_key = frappe.utils.sha256_hash(key)
invitation_name = frappe.db.get_value("User Invitation", filters={"key": hashed_key})
if not invitation_name:
frappe.throw(title=_("Error"), msg=_("Invalid key"))
frappe.throw(title=_("Error"), msg=_("Invalid or expired key"))
invitation = frappe.get_doc("User Invitation", invitation_name)
# accept invitation
@ -143,7 +143,7 @@ def _accept_invitation(key: str, in_test: bool) -> None:
# set redirect_to
redirect_to = frappe.utils.get_url(invitation.get_redirect_to_path())
if should_update_password:
redirect_to = f"{user.reset_password()}&redirect_to=/{invitation.get_redirect_to_path()}"
redirect_to = f"{user._reset_password()}&redirect_to=/{invitation.get_redirect_to_path()}"
# GET requests do not cause an implicit commit
frappe.db.commit() # nosemgrep

View file

@ -20,6 +20,22 @@ class CommunicationEmailMixin:
parent_doc = get_parent_doc(self)
return parent_doc.owner if parent_doc else None
def get_notification_recipient(self):
"""Get notification recipient of the communication docs parent.
Calls `get_notification_email` on the parent if available; otherwise returns the owner.
This uses `run_method` so hooks can customize recipients per app/site.
"""
parent_doc = get_parent_doc(self)
if not parent_doc:
return None
notification_email = parent_doc.run_method("get_notification_email")
if notification_email:
return notification_email
return parent_doc.owner
def get_all_email_addresses(self, exclude_displayname=False):
"""Get all Email addresses mentioned in the doc along with display name."""
return (
@ -60,7 +76,7 @@ class CommunicationEmailMixin:
"""Build cc list to send an email.
* if email copy is requested by sender, then add sender to CC.
* If this doc is created through inbound mail, then add doc owner to cc list
* If this doc is created through inbound mail, then add the notification recipient to CC
* remove all the thread_notify disabled users.
* Remove standard users from email list
"""
@ -77,9 +93,9 @@ class CommunicationEmailMixin:
cc.append(sender)
if is_inbound_mail_communcation:
# inform parent document owner incase communication is created through inbound mail
if doc_owner := self.get_owner():
cc.append(doc_owner)
# inform the configured notification recipient in case communication is created inbound
if notification_recipient := self.get_notification_recipient():
cc.append(notification_recipient)
cc = set(cc) - {self.sender_mailid}
assignees = set(self.get_assignees()) - {self.sender_mailid}
# Check and remove If user disabled notifications for incoming emails on assigned document.

View file

@ -228,7 +228,7 @@
],
"grid_page_length": 50,
"links": [],
"modified": "2026-03-31 20:37:16.503023",
"modified": "2026-04-09 11:13:35.484376",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",
@ -240,6 +240,7 @@
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,

View file

@ -7,7 +7,7 @@ from frappe.model import display_fieldtypes, no_value_fields
from frappe.model import table_fields as table_fieldtypes
from frappe.utils import flt, format_duration, groupby_metric
from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response
from frappe.utils.xlsxutils import build_xlsx_response, get_default_xlsx_styles
class Exporter:
@ -253,7 +253,17 @@ class Exporter:
if self.file_type == "CSV":
build_csv_response(self.get_csv_array_for_export(), _(self.doctype))
elif self.file_type == "Excel":
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))
data = self.get_csv_array_for_export()
styles = get_default_xlsx_styles(
columns=self.fields,
# exclude header row
data=data[1:],
# from the second child row onwards, parent values will be empty
# so currency value from parent doc may be absent, avoid inconsistency
currency_formatting=False,
)
build_xlsx_response(data, _(self.doctype), styles=styles)
def group_children_data_by_parent(self, children_data: dict[str, list]):
return groupby_metric(children_data, key="parent")

View file

@ -13,6 +13,7 @@ from frappe.core.doctype.version.version import get_diff
from frappe.model import no_value_fields
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content
from frappe.utils.data import escape_html
from frappe.utils.xlsxutils import (
read_xls_file_from_attached_file,
read_xlsx_file_from_attached_file,
@ -727,7 +728,9 @@ class Row:
elif df.fieldtype == "Link":
exists = self.link_exists(value, df)
if not exists:
msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options))
msg = _("Value {0} missing for {1}").format(
frappe.bold(escape_html(cstr(value))), frappe.bold(df.options)
)
self.warnings.append(
{
"row": self.row_number,
@ -746,7 +749,8 @@ class Row:
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must in {1} format").format(
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
frappe.bold(escape_html(cstr(value))),
frappe.bold(get_user_format(col.date_format)),
),
}
)
@ -761,7 +765,8 @@ class Row:
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must in {1} format").format(
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
frappe.bold(escape_html(cstr(value))),
frappe.bold(get_user_format(col.date_format)),
),
}
)
@ -774,7 +779,7 @@ class Row:
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value)
frappe.bold(escape_html(cstr(value)))
),
}
)
@ -1045,7 +1050,7 @@ class Column:
]
not_exists = list(set(values) - set(exists))
if not_exists:
missing_values = ", ".join(not_exists)
missing_values = ", ".join(escape_html(v) for v in not_exists)
message = _("The following values do not exist for {0}: {1}")
self.warnings.append(
{
@ -1088,7 +1093,7 @@ class Column:
invalid = values - set(options)
if invalid:
valid_values = ", ".join(frappe.bold(o) for o in options)
invalid_values = ", ".join(frappe.bold(i) for i in invalid)
invalid_values = ", ".join(frappe.bold(escape_html(i)) for i in invalid)
message = _("The following values are invalid: {0}. Values must be one of {1}")
self.warnings.append(
{

View file

@ -41,6 +41,7 @@
"print_hide",
"print_hide_if_no_value",
"report_hide",
"in_import_template",
"column_break_28",
"depends_on",
"collapsible",
@ -640,6 +641,13 @@
"fieldname": "show_description_on_click",
"fieldtype": "Check",
"label": "Show Description on Click"
},
{
"default": "0",
"description": "Enable this option to include the field in the data import template",
"fieldname": "in_import_template",
"fieldtype": "Check",
"label": "Include in Import Template"
}
],
"grid_page_length": 50,
@ -647,7 +655,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-10 21:39:58.400441",
"modified": "2026-04-24 13:21:02.590853",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -83,6 +83,7 @@ class DocField(Document):
ignore_xss_filter: DF.Check
in_filter: DF.Check
in_global_search: DF.Check
in_import_template: DF.Check
in_list_view: DF.Check
in_preview: DF.Check
in_standard_filter: DF.Check

View file

@ -3,9 +3,12 @@
frappe.ui.form.on("DocType", {
onload: function (frm) {
if (frm.is_new() && !frm.doc?.fields) {
if (frm.is_new()) {
frm.set_value("allow_auto_repeat", 0);
if (!frm.doc?.fields) {
frappe.listview_settings["DocType"].new_doctype_dialog();
}
}
frm.call("check_pending_migration");
},

View file

@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-02-18 13:36:19",
@ -34,6 +35,7 @@
"quick_entry",
"grid_page_length",
"rows_threshold_for_grid_search",
"allow_bulk_edit",
"cb01",
"track_changes",
"track_seen",
@ -665,6 +667,7 @@
"label": "Sender Name Field"
},
{
"depends_on": "eval:!doc.istable",
"fieldname": "permissions_tab",
"fieldtype": "Tab Break",
"label": "Permissions"
@ -714,6 +717,14 @@
"fieldname": "recipient_account_field",
"fieldtype": "Data",
"label": "Recipient Account Field"
},
{
"default": "1",
"depends_on": "istable",
"description": "Enable bulk update of this field across child table rows.",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
}
],
"grid_page_length": 50,
@ -792,7 +803,7 @@
"link_fieldname": "document_type"
}
],
"modified": "2025-09-23 06:48:13.555017",
"modified": "2026-04-20 16:06:57.212832",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -823,6 +834,7 @@
],
"route": "doctype",
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"search_fields": "module",
"show_name_in_global_search": 1,
"sort_field": "creation",

View file

@ -101,6 +101,7 @@ class DocType(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_events_in_timeline: DF.Check
allow_guest_to_view: DF.Check
@ -550,11 +551,20 @@ class DocType(Document):
and (frappe.conf.developer_mode or frappe.flags.allow_doctype_export)
)
if allow_doctype_export:
def export_doctype_files():
self.export_doc()
self.make_controller_template()
self.set_base_class_for_controller()
self.export_types_to_controller()
request = getattr(frappe.local, "request", None)
# Defer file writes until after the response so the client can sync the saved doc first.
if request and hasattr(request, "after_response"):
request.after_response.add(export_doctype_files)
else:
export_doctype_files()
# update index
if not self.custom:
self.run_module_method("on_doctype_update")

View file

@ -48,7 +48,13 @@ frappe.ui.form.on("File", {
const field = frm.get_field("attached_to_name");
field.$input_wrapper
.find(".control-value")
.html(`${frappe.utils.get_form_link(frm.doctype, frm.docname, true)}`);
.html(
`${frappe.utils.get_form_link(
frm.doc.attached_to_doctype,
frm.doc.attached_to_name,
true
)}`
);
}
},

View file

@ -67,7 +67,8 @@
"fieldname": "is_home_folder",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Home Folder"
"label": "Is Home Folder",
"search_index": 1
},
{
"default": "0",
@ -190,7 +191,7 @@
"icon": "fa fa-file",
"idx": 1,
"links": [],
"modified": "2025-01-15 11:46:42.917146",
"modified": "2026-04-15 19:56:45.317786",
"modified_by": "Administrator",
"module": "Core",
"name": "File",

View file

@ -111,10 +111,21 @@ class File(Document):
self.validate_attachment_limit()
self.set_file_type()
self.validate_file_extension()
self.validate_private_file_access()
if self.is_folder:
return
if self.flags.copy_from_existing_file:
# Preserve the normal insert lifecycle for hooks and validations, but skip
# reprocessing an existing blob that is already referenced by `file_url`.
if not self.file_url:
frappe.throw(
_("File URL is required when copying an existing attachment."),
exc=frappe.MandatoryError,
)
return
if self.is_remote_file:
self.validate_remote_file()
else:
@ -128,6 +139,29 @@ class File(Document):
if not self.is_folder:
self.create_attachment_record()
def create_attachment_copy(
self,
attached_to_doctype: str,
attached_to_name: str,
attached_to_field: str | None = None,
ignore_permissions: bool = False,
):
"""Efficiently copy an attachment from one document to another by reusing `file_url`."""
if self.is_folder:
frappe.throw(_("Cannot attach a folder to a document"))
attachment = frappe.copy_doc(self)
attachment.update(
{
"attached_to_doctype": attached_to_doctype,
"attached_to_name": attached_to_name,
"attached_to_field": attached_to_field,
}
)
attachment.folder = None
attachment.flags.copy_from_existing_file = True
return attachment.insert(ignore_permissions=ignore_permissions)
def validate(self):
if self.is_folder:
return
@ -167,6 +201,36 @@ class File(Document):
except PermissionError:
frappe.throw(_("Only System Managers can make this file public."))
def validate_private_file_access(self):
"""Validate that the user has permission to access an existing private file."""
if not self.file_url:
return
existing_files = frappe.get_all(
"File",
filters={"file_url": self.file_url},
fields=["name", "owner", "is_private"],
limit=1,
)
if not existing_files:
return
existing_file = existing_files[0]
if existing_file.is_private:
user = frappe.session.user
if user == existing_file.owner or user == "Administrator":
return
existing_doc = frappe.get_doc("File", existing_file.name)
if not has_permission(existing_doc, "read", user=user):
frappe.throw(
_("You do not have permission to access this file"),
frappe.PermissionError,
)
def after_rename(self, *args, **kwargs):
for successor in self.get_successors():
setup_folder_path(successor, self.name)

View file

@ -254,6 +254,66 @@ class TestSameContent(IntegrationTestCase):
limit_property.delete()
frappe.clear_cache(doctype="ToDo")
def test_create_attachment_copy(self):
doctype, docname = make_test_doc()
source_file = frappe.get_doc(
{
"doctype": "File",
"file_name": f"existing-file-{frappe.generate_hash(length=8)}.txt",
"content": "Existing attachment content",
}
).insert()
comment_count_before = frappe.db.count(
"Comment", {"reference_doctype": doctype, "reference_name": docname}
)
copied_file = source_file.create_attachment_copy(doctype, docname)
comment_count_after = frappe.db.count(
"Comment", {"reference_doctype": doctype, "reference_name": docname}
)
self.assertNotEqual(copied_file.name, source_file.name)
self.assertEqual(copied_file.file_url, source_file.file_url)
self.assertEqual(copied_file.attached_to_doctype, doctype)
self.assertEqual(copied_file.attached_to_name, docname)
self.assertEqual(
copied_file.folder,
frappe.db.get_value("File", {"is_attachments_folder": 1}),
)
self.assertEqual(comment_count_after, comment_count_before + 1)
def test_create_attachment_copy_respects_attachment_limit(self):
doctype, docname = make_test_doc()
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
limit_property = make_property_setter("ToDo", None, "max_attachments", 1, "int", for_doctype=True)
source_file_1 = frappe.get_doc(
{
"doctype": "File",
"file_name": f"existing-limit-file-{frappe.generate_hash(length=8)}.txt",
"content": "Existing attachment content 1",
}
).insert()
source_file_2 = frappe.get_doc(
{
"doctype": "File",
"file_name": f"existing-limit-file-{frappe.generate_hash(length=8)}.txt",
"content": "Existing attachment content 2",
}
).insert()
try:
source_file_1.create_attachment_copy(doctype, docname)
self.assertRaises(
frappe.exceptions.AttachmentLimitReached,
source_file_2.create_attachment_copy,
doctype,
docname,
)
finally:
limit_property.delete()
frappe.clear_cache(doctype="ToDo")
def test_utf8_bom_content_decoding(self):
utf8_bom_content = test_content1.encode("utf-8-sig")
_file: frappe.Document = frappe.get_doc(

View file

@ -480,3 +480,16 @@ def find_file_by_url(path: str, name: str | None = None) -> "File" | None:
def get_safe_file_name(file_name: str) -> str:
return re.sub(r"[/\\%?#]", "_", file_name)
def check_path_safety(base_path: str, requested_path: str) -> bool:
"""Util to check path safety by ensuring sandboxing and logging unsuccessful attempts"""
base_path = os.path.realpath(base_path)
requested_path = os.path.realpath(requested_path)
if os.path.commonpath([base_path, requested_path]) != base_path:
frappe.log_error(
title="Attempted Unauthorized File Access",
message=f"Blocked access to: {requested_path}",
)
return False
return True

View file

@ -93,6 +93,7 @@ class PackageRelease(Document):
def export_package_files(self, package):
# write readme
if package.readme:
with open(frappe.get_site_path("packages", package.package_name, "README.md"), "w") as readme:
readme.write(package.readme)

View file

@ -25,10 +25,9 @@
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-13 16:17:58.536849",
"modified": "2026-04-27 13:30:28.567106",
"modified_by": "Administrator",
"module": "Core",
"name": "Permission Type",

View file

@ -87,7 +87,10 @@ class PreparedReport(Document):
)
def get_prepared_data(self, with_file_name=False):
if attachments := get_attachments(self.doctype, self.name):
attachments = get_attachments(self.doctype, self.name)
if not attachments:
frappe.throw(_("No attachment found for the prepared report"), title=_("Attachment Not Found"))
attachment = None
for f in attachments or []:
if f.file_url.endswith(".gz"):
@ -141,7 +144,10 @@ def generate_report(prepared_report):
except Exception:
# we need to ensure that error gets stored
_save_error(instance, error=frappe.get_traceback(with_context=True))
return
instance.reload()
instance.status = "Completed"
instance.report_end_time = frappe.utils.now()
instance.peak_memory_usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
add_data_to_monitor(peak_memory_usage=instance.peak_memory_usage)

View file

@ -55,6 +55,30 @@ frappe.ui.form.on("Report", {
},
};
});
frm.set_query("default_print_format", () => {
return {
filters: {
print_format_for: "Report",
report: frm.doc.name,
print_format_type: "JS",
disabled: 0,
},
};
});
frm.set_query("letter_head", () => {
const filters = {
letter_head_for: "Report",
disabled: 0,
};
if (frm.doc.is_standard === "Yes") {
filters.standard = "Yes";
}
return { filters };
});
},
ref_doctype: function (frm) {

View file

@ -14,6 +14,7 @@
"column_break_4",
"report_type",
"letter_head",
"default_print_format",
"add_total_row",
"disabled",
"prepared_report",
@ -96,10 +97,9 @@
"label": "Disabled"
},
{
"depends_on": "eval: doc.is_standard == \"No\"",
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"label": "Default Letter Head",
"options": "Letter Head"
},
{
@ -202,12 +202,18 @@
"fieldname": "add_translate_data",
"fieldtype": "Check",
"label": "Add Translate Data"
},
{
"fieldname": "default_print_format",
"fieldtype": "Link",
"label": "Default Print Format",
"options": "Print Format"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-28 18:28:32.510719",
"modified": "2026-04-10 00:03:15.212213",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",

View file

@ -5,16 +5,17 @@ import json
import threading
import frappe
import frappe.desk.query_report
from frappe import _, scrub
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from frappe.core.doctype.page.page import delete_custom_role
from frappe.desk.query_report import run
from frappe.desk.reportview import append_totals_row
from frappe.model.document import Document
from frappe.modules import make_boilerplate
from frappe.modules.export_file import export_to_files
from frappe.utils import cint, cstr
from frappe.utils.safe_exec import check_safe_sql_query, safe_exec
from frappe.utils.xlsxutils import XLSXMetadata, XLSXStyleBuilder
class Report(Document):
@ -32,6 +33,7 @@ class Report(Document):
add_total_row: DF.Check
add_translate_data: DF.Check
columns: DF.Table[ReportColumn]
default_print_format: DF.Link | None
disabled: DF.Check
filters: DF.Table[ReportFilter]
is_standard: DF.Literal["No", "Yes"]
@ -72,16 +74,17 @@ class Report(Document):
frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report"))
if self.is_standard == "Yes":
if frappe.session.user != "Administrator":
frappe.throw(_("Only Administrator can save a standard report. Please rename and save."))
# Letter Head is visible only for non-standard reports.
# It should not remain set when it's invisible.
self.letter_head = None
self.validate_standard_report()
if self.report_type == "Report Builder":
self.update_report_json()
if self.default_print_format and self.has_value_changed("default_print_format"):
self.validate_default_print_format()
if self.letter_head and self.has_value_changed("letter_head"):
self.validate_letter_head()
def before_insert(self):
self.set_doctype_roles()
@ -89,7 +92,6 @@ class Report(Document):
self.export_doc()
def before_export(self, doc):
doc.letter_head = None
doc.prepared_report = 0
def on_trash(self):
@ -106,6 +108,13 @@ class Report(Document):
delete_custom_role("report", self.name)
def clear_cache(self):
self.update_report_cache()
return super().clear_cache()
def update_report_cache(self):
frappe.cache.delete_key("bootinfo")
def delete_report_folder(self):
from frappe.modules.export_file import delete_folder
@ -202,11 +211,14 @@ class Report(Document):
return res
def get_module_method(self, method):
module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module")
method_path = get_report_module_dotted_path(module, self.name) + "." + method
return frappe.get_attr(method_path)
def execute_module(self, filters):
# report in python module
module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module")
method_name = get_report_module_dotted_path(module, self.name) + ".execute"
return frappe.get_attr(method_name)(frappe._dict(filters))
return self.get_module_method("execute")(frappe._dict(filters))
def execute_script(self, filters):
# server script
@ -242,7 +254,7 @@ class Report(Document):
self, filters=None, user=None, ignore_prepared_report=False, are_default_filters=True
):
columns, result = [], []
data = frappe.desk.query_report.run(
data = run(
self.name,
filters=filters,
user=user,
@ -314,8 +326,6 @@ class Report(Document):
columns = params.get("fields")
elif params.get("columns"):
columns = params.get("columns")
elif params.get("fields"):
columns = params.get("fields")
else:
columns = [["name", self.ref_doctype]]
columns.extend(
@ -401,6 +411,53 @@ class Report(Document):
return data
def validate_standard_report(self):
if frappe.session.user != "Administrator":
frappe.throw(_("Only Administrator can save a standard report. Please rename and save."))
if not cint(frappe.conf.developer_mode):
frappe.throw(_("Standard reports can only be created in developer mode."))
def validate_default_print_format(self):
pf = frappe.db.get_value(
"Print Format",
self.default_print_format,
["report", "print_format_for", "print_format_type", "disabled"],
as_dict=True,
)
if (
not pf
or pf.report != self.name
or pf.print_format_for != "Report"
or pf.print_format_type != "JS"
or pf.disabled
):
frappe.throw(_("Selected Print Format is invalid for this Report."))
def validate_letter_head(self):
if not self.letter_head:
return
letter_head = frappe.db.get_value(
"Letter Head",
self.letter_head,
["letter_head_for", "standard", "disabled"],
as_dict=True,
)
if (
not letter_head
or letter_head.letter_head_for != "Report"
or (self.is_standard == "Yes" and letter_head.standard != "Yes")
or letter_head.disabled
):
frappe.throw(
_("Selected Letter Head '{0}' is invalid for '{1}' Report.").format(
self.letter_head, self.name
)
)
@frappe.whitelist()
def toggle_disable(self, disable: bool):
if not self.has_permission("write"):
@ -408,6 +465,18 @@ class Report(Document):
self.db_set("disabled", cint(disable))
def get_xlsx_styles_from_module(self, metadata: XLSXMetadata) -> dict:
if self.is_standard != "Yes" or self.report_type not in ("Query Report", "Script Report"):
return
try:
method = self.get_module_method("get_xlsx_styles")
except AttributeError:
# Ignore if hook(method) is not defined
return
return method(metadata)
def is_prepared_report_enabled(report):
return cint(frappe.db.get_value("Report", report, "prepared_report"))

View file

@ -406,3 +406,32 @@ result = [
self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)
def test_report_cache_invalidation(self):
import frappe.sessions
from frappe.utils import set_request
frappe.set_user("test@example.com")
set_request(method="GET", path="/app")
try:
frappe.sessions.get()
report_name = _save_report(
"Test Cache Invalidation Report",
"User",
json.dumps([{"fieldname": "email", "fieldtype": "Data", "label": "Email"}]),
)
cached_bootinfo = frappe.sessions.get()
self.assertIn(report_name, cached_bootinfo["user"]["all_reports"])
doc = frappe.get_doc("Report", report_name)
delete_report(doc.name)
cached_bootinfo = frappe.sessions.get()
self.assertNotIn(report_name, cached_bootinfo["user"]["all_reports"])
finally:
frappe.local.request = None
frappe.set_user("Administrator")

View file

@ -0,0 +1,20 @@
// Copyright (c) 2026, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Security Settings", {
refresh(frm) {
const wrapper = frm.fields_dict.securitytxt_section.wrapper;
if ($(wrapper).find(".security-txt-banner").length) return;
$(wrapper)
.find(".section-body")
.prepend(
`<div class="alert alert-warning border d-flex justify-content-between align-items-center security-txt-banner" style="flex: 0 0 100%; max-width: 100%; border-color: var(--border-color);">
<span>${__("Security.txt will be served only under HTTPS.")}</span>
<a href="https://tools.ietf.org/html/rfc9116#section-6.7" target="_blank" class="btn btn-xs btn-secondary">${__(
"Learn more"
)}</a>
</div>`
);
},
});

View file

@ -0,0 +1,82 @@
{
"actions": [],
"creation": "2026-04-10 16:14:40.343135",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"securitytxt_section",
"public_expires",
"public_contacts",
"public_languages",
"public_policy",
"security_txt"
],
"fields": [
{
"fieldname": "securitytxt_section",
"fieldtype": "Section Break",
"label": "Security.txt"
},
{
"description": "Date after which this security.txt should be considered stale. Expires timestamp is converted to UTC.",
"fieldname": "public_expires",
"fieldtype": "Datetime",
"label": "Expires"
},
{
"description": "Website, email or phone where vulnerabilities can be reported. Defaults to `https://security.frappe.io`",
"fieldname": "public_contacts",
"fieldtype": "Table",
"label": "Contact",
"options": "Security Settings Contact"
},
{
"description": "Defaults to `en`",
"fieldname": "public_languages",
"fieldtype": "Table MultiSelect",
"label": "Preferred Language",
"options": "Security Settings Language"
},
{
"description": "Guidelines and policies on vulnerability reporting. Defaults to `https://frappe.io/security`",
"fieldname": "public_policy",
"fieldtype": "Data",
"label": "Policy",
"options": "URL"
},
{
"fieldname": "security_txt",
"fieldtype": "Small Text",
"is_virtual": 1,
"label": "Preview",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-04-17 13:07:45.259146",
"modified_by": "Administrator",
"module": "Core",
"name": "Security Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -0,0 +1,122 @@
# Copyright (c) 2026, Frappe Technologies and contributors
# For license information, please see license.txt
from datetime import UTC, datetime
from zoneinfo import ZoneInfo
import frappe
import frappe.utils
from frappe import _
from frappe.model.document import Document
from frappe.utils import (
get_system_timezone,
now_datetime,
validate_email_address,
validate_phone_number,
validate_url,
)
class SecuritySettings(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.core.doctype.security_settings_contact.security_settings_contact import (
SecuritySettingsContact,
)
from frappe.core.doctype.security_settings_language.security_settings_language import (
SecuritySettingsLanguage,
)
from frappe.types import DF
public_contacts: DF.Table[SecuritySettingsContact]
public_expires: DF.Datetime | None
public_languages: DF.TableMultiSelect[SecuritySettingsLanguage]
public_policy: DF.Data | None
# end: auto-generated types
@property
def security_txt(self):
return (
"\n\n".join(
[
self.public_policy_section,
self.public_contacts_section,
self.public_languages_section,
self.public_expires_section,
]
)
+ "\n"
)
@property
def public_policy_section(self):
value = self.public_policy or "https://frappe.io/security"
return f"# Read our security policy before reporting an issue\nPolicy: {value}"
@property
def public_contacts_section(self):
contacts = [self.with_protocol(c.contact, c.type) for c in self.public_contacts] or [
"https://security.frappe.io"
]
value = "\n".join(f"Contact: {c}" for c in contacts)
return f"# Our security address\n{value}"
@property
def public_languages_section(self):
langs = [l.language for l in self.public_languages] or ["en"]
value = ", ".join(langs)
return f"# We prefer talking in\nPreferred-Languages: {value}"
@property
def public_expires_section(self):
expires = self.public_expires or frappe.utils.add_years(frappe.utils.now_datetime(), 1)
if isinstance(expires, str):
expires = datetime.fromisoformat(expires)
expires = expires.replace(microsecond=0, tzinfo=ZoneInfo(get_system_timezone())).astimezone(UTC)
value = expires.strftime("%Y-%m-%dT%H:%M:%SZ")
return f"Expires: {value}"
def with_protocol(self, url: str, type_: str) -> str:
"""Prefix the URL with the appropriate protocol based on the contact type."""
match type_:
case "Email":
if not url.startswith("mailto:"):
return f"mailto:{url}"
case "Phone":
if not url.startswith("tel:"):
return f"tel:{url}"
return url
def validate(self):
self.validate_public_policy()
self.validate_public_contacts()
self.validate_expires()
def validate_public_policy(self):
if self.public_policy:
if not self.public_policy.startswith("https://"):
frappe.throw(_("Public Policy URL must start with https://"))
def validate_public_contacts(self):
for contact in self.public_contacts:
match contact.type:
case "Email":
validate_email_address(contact.contact, throw=True)
case "Phone":
validate_phone_number(contact.contact, throw=True)
case "Website":
validate_url(contact.contact, throw=True)
if not contact.contact.startswith("https://"):
frappe.throw(_("URL contact must start with https://"))
def validate_expires(self):
if self.public_expires:
expires = self.public_expires
if isinstance(expires, str):
expires = datetime.fromisoformat(expires)
if expires <= now_datetime():
frappe.throw(_("Expiration date must be in the future"))

View file

@ -0,0 +1,43 @@
# Copyright (c) 2026, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
from frappe.utils import get_datetime, now_datetime
from frappe.utils.user import get_users_with_role
def check_security_txt_expiry():
security_settings = frappe.get_doc("Security Settings")
if not security_settings.public_expires:
return
expires = security_settings.public_expires
if isinstance(expires, str):
expires = get_datetime(expires)
now = now_datetime()
days_until_expiry = (expires - now).days
alert_days = [30, 15, 7, 1]
if days_until_expiry in alert_days:
send_expiry_alert(frappe.local.site, expires, days_until_expiry)
def send_expiry_alert(site: str, expires, days_until_expiry: int):
recipients = get_users_with_role("System Manager")
if not recipients:
return
subject = get_email_subject(site, days_until_expiry)
frappe.sendmail(
recipients=recipients,
subject=subject,
template="security_txt_expiry_alert",
args={
"site": site,
"expires": expires,
"days_remaining": days_until_expiry,
},
)
def get_email_subject(site: str, days_until_expiry: int) -> str:
if days_until_expiry == 1:
return f"[URGENT] Security.txt expires in 1 day - {site}"
return f"Security.txt expires in {days_until_expiry} days - {site}"

View file

@ -0,0 +1,272 @@
# Copyright (c) 2026, Frappe Technologies and Contributors
# License: MIT. See LICENSE
from datetime import UTC, datetime, timedelta
import frappe
from frappe.tests import IntegrationTestCase
class TestSecuritySettings(IntegrationTestCase):
def test_public_policy_section_default(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_policy": None,
}
)
section = doc.public_policy_section
self.assertIn("Policy: https://frappe.io/security", section)
def test_public_policy_section_custom(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_policy": "https://example.com/security-policy",
}
)
section = doc.public_policy_section
self.assertIn("Policy: https://example.com/security-policy", section)
def test_public_languages_section_default(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
section = doc.public_languages_section
self.assertIn("Preferred-Languages: en", section)
def test_public_languages_section_custom(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_languages": [
{"language": "en"},
{"language": "fr"},
],
}
)
section = doc.public_languages_section
self.assertIn("Preferred-Languages: en, fr", section)
def test_public_contacts_section_default(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
section = doc.public_contacts_section
self.assertIn("https://security.frappe.io", section)
def test_public_contacts_section_email(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Email", "contact": "security@example.com"},
],
}
)
section = doc.public_contacts_section
self.assertIn("mailto:security@example.com", section)
def test_public_contacts_section_phone(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Phone", "contact": "+1234567890"},
],
}
)
section = doc.public_contacts_section
self.assertIn("tel:+1234567890", section)
def test_public_contacts_section_website(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Website", "contact": "https://security.example.com"},
],
}
)
section = doc.public_contacts_section
self.assertIn("https://security.example.com", section)
def test_with_protocol_email_without_protocol(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
result = doc.with_protocol("security@example.com", "Email")
self.assertEqual(result, "mailto:security@example.com")
def test_with_protocol_email_with_protocol(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
result = doc.with_protocol("mailto:security@example.com", "Email")
self.assertEqual(result, "mailto:security@example.com")
def test_with_protocol_phone_without_protocol(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
result = doc.with_protocol("+1234567890", "Phone")
self.assertEqual(result, "tel:+1234567890")
def test_with_protocol_phone_with_protocol(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
result = doc.with_protocol("tel:+1234567890", "Phone")
self.assertEqual(result, "tel:+1234567890")
def test_with_protocol_website(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
result = doc.with_protocol("https://example.com", "Website")
self.assertEqual(result, "https://example.com")
def test_security_txt_full(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_policy": "https://example.com/policy",
"public_contacts": [
{"type": "Email", "contact": "security@example.com"},
],
"public_languages": [
{"language": "en"},
],
"public_expires": datetime.now() + timedelta(days=365),
}
)
security_txt = doc.security_txt
self.assertIn("Policy: https://example.com/policy", security_txt)
self.assertIn("mailto:security@example.com", security_txt)
self.assertIn("Preferred-Languages: en", security_txt)
self.assertIn("Expires:", security_txt)
def test_validate_public_policy_with_http(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_policy": "http://example.com",
}
)
self.assertRaises(frappe.ValidationError, doc.validate_public_policy)
def test_validate_public_policy_with_https(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_policy": "https://example.com",
}
)
# Should not raise
doc.validate_public_policy()
def test_validate_public_contacts_invalid_email(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Email", "contact": "invalid-email"},
],
}
)
self.assertRaises(frappe.ValidationError, doc.validate_public_contacts)
def test_validate_public_contacts_valid_email(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Email", "contact": "security@example.com"},
],
}
)
# Should not raise
doc.validate_public_contacts()
def test_validate_public_contacts_invalid_phone(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Phone", "contact": "not-a-phone"},
],
}
)
self.assertRaises(frappe.ValidationError, doc.validate_public_contacts)
def test_validate_public_contacts_valid_phone(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Phone", "contact": "+1234567890"},
],
}
)
# Should not raise
doc.validate_public_contacts()
def test_validate_public_contacts_website_without_https(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Website", "contact": "http://example.com"},
],
}
)
self.assertRaises(frappe.ValidationError, doc.validate_public_contacts)
def test_validate_public_contacts_valid_website(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_contacts": [
{"type": "Website", "contact": "https://example.com"},
],
}
)
# Should not raise
doc.validate_public_contacts()
def test_validate_expires_past(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_expires": datetime.now() - timedelta(days=1),
}
)
self.assertRaises(frappe.ValidationError, doc.validate_expires)
def test_validate_expires_future(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_expires": datetime.now() + timedelta(days=365),
}
)
# Should not raise
doc.validate_expires()
@IntegrationTestCase.change_settings("System Settings", {"time_zone": "Etc/UTC"})
def test_public_expires_section_future_date(self):
from datetime import timezone
future_date = datetime(2027, 12, 31, 23, 59, 59)
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_expires": future_date,
}
)
section = doc.public_expires_section
self.assertIn("2027-12-31T23:59:59Z", section)
@IntegrationTestCase.change_settings("System Settings", {"time_zone": "Asia/Kolkata"})
def test_public_expires_section_string(self):
doc = frappe.get_doc(
{
"doctype": "Security Settings",
"public_expires": "2028-01-01T05:29:59",
}
)
section = doc.public_expires_section
self.assertIn("2027-12-31T23:59:59Z", section)
def test_public_expires_section_default(self):
doc = frappe.get_doc({"doctype": "Security Settings"})
section = doc.public_expires_section
# Default is 1 year from now
self.assertIn("Expires:", section)
self.assertIn("T", section) # ISO format

View file

@ -0,0 +1,43 @@
{
"actions": [],
"creation": "2026-04-11 13:06:29.308243",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"type",
"contact"
],
"fields": [
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Website\nEmail\nPhone",
"reqd": 1
},
{
"fieldname": "contact",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Contact",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-14 12:50:25.814560",
"modified_by": "Administrator",
"module": "Core",
"name": "Security Settings Contact",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,24 @@
# Copyright (c) 2026, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SecuritySettingsContact(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
contact: DF.Data
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
type: DF.Literal["Website", "Email", "Phone"]
# end: auto-generated types
pass

View file

@ -0,0 +1,35 @@
{
"actions": [],
"creation": "2026-04-11 12:53:09.006649",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"language"
],
"fields": [
{
"fieldname": "language",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Language",
"options": "Language",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-14 12:50:44.554462",
"modified_by": "Administrator",
"module": "Core",
"name": "Security Settings Language",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,23 @@
# Copyright (c) 2026, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SecuritySettingsLanguage(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
language: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -168,9 +168,9 @@ def queue_submission(doc: Document, action: str, alert: bool = True):
"Submission Queue", {"ref_doctype": doc.doctype, "ref_docname": doc.name, "status": "Queued"}
):
frappe.msgprint(
_(
"This document has already been queued for submission. You can track the progress over {0}."
).format(f"<a href='/desk/submission-queue/{existing_queue}'><b>here</b></a>"),
_("This document has already been queued for {0}. You can track the progress over {1}.").format(
action, f"<a href='/desk/submission-queue/{existing_queue}'><b>here</b></a>"
),
indicator="orange",
alert=True,
)
@ -183,8 +183,8 @@ def queue_submission(doc: Document, action: str, alert: bool = True):
if alert:
frappe.msgprint(
_("Queued for Submission. You can track the progress over {0}.").format(
f"<a href='/desk/submission-queue/{queue.name}'><b>here</b></a>"
_("Queued for {0}. You can track the progress over {1}.").format(
action, f"<a href='/desk/submission-queue/{queue.name}'><b>here</b></a>"
),
indicator="green",
alert=True,

View file

@ -51,3 +51,71 @@ class TestSubmissionQueue(IntegrationTestCase):
job = self.queue.fetch_job(submission_queue.job_id)
# Test completion
self.check_status(job, status="finished")
def test_cancel_operation(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
if not frappe.db.table_exists("Test Submission Queue", cached=False):
doc = new_doctype("Test Submission Queue", is_submittable=True, queue_in_background=True)
doc.insert()
d = frappe.new_doc("Test Submission Queue")
d.update({"some_fieldname": "Random"})
d.insert()
d.submit()
frappe.db.commit()
self.assertEqual(d.docstatus, 1)
queue_submission(d, "Cancel")
frappe.db.commit()
time.sleep(4)
submission_queue = frappe.get_last_doc("Submission Queue")
job = self.queue.fetch_job(submission_queue.job_id)
self.check_status(job, status="finished")
d.reload()
self.assertEqual(d.docstatus, 2)
def test_cancel_on_cancelled_doc(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
if not frappe.db.table_exists("Test Submission Queue", cached=False):
doc = new_doctype("Test Submission Queue", is_submittable=True, queue_in_background=True)
doc.insert()
d = frappe.new_doc("Test Submission Queue")
d.update({"some_fieldname": "Random"})
d.insert()
d.submit()
frappe.db.commit()
existing = frappe.get_doc(
{
"doctype": "Submission Queue",
"ref_doctype": d.doctype,
"ref_docname": d.name,
"status": "Queued",
}
)
existing.insert(d, "Cancel")
frappe.db.commit()
initial_count = frappe.db.count(
"Submission Queue", {"ref_doctype": d.doctype, "ref_docname": d.name, "status": "Queued"}
)
queue_submission(d, "Cancel")
final_count = frappe.db.count(
"Submission Queue", {"ref_doctype": d.doctype, "ref_docname": d.name, "status": "Queued"}
)
self.assertEqual(initial_count, final_count)
existing.delete(ignore_permissions=True)
frappe.db.commit()

View file

@ -19,7 +19,7 @@ frappe.ui.form.on("System Settings", {
frappe.xcall("frappe.apps.get_apps").then((r) => {
let apps = r?.map((r) => r.name) || [];
frm.set_df_property("default_app", "options", [" ", ...apps]);
frm.set_df_property("default_app", "options", ["", ...apps]);
});
frm.trigger("set_rounding_method_options");

View file

@ -114,6 +114,8 @@
"enable_telemetry",
"search_section",
"link_field_results_limit",
"column_break_nebx",
"allow_clearing_link_fields",
"api_logging_section",
"log_api_requests"
],
@ -783,13 +785,23 @@
"fieldname": "only_allow_system_managers_to_upload_public_files",
"fieldtype": "Check",
"label": "Only allow System Managers to upload public files"
},
{
"fieldname": "column_break_nebx",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Adds a clear (\u00d7) button to Link fields, allowing users to quickly remove the selected value.",
"fieldname": "allow_clearing_link_fields",
"fieldtype": "Check",
"label": "Allow Clearing Link Fields"
}
],
"hide_toolbar": 1,
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2026-02-24 14:27:04.763075",
"modified": "2026-04-14 16:26:19.634212",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -18,6 +18,7 @@ class SystemSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
allow_clearing_link_fields: DF.Check
allow_consecutive_login_attempts: DF.Int
allow_error_traceback: DF.Check
allow_guests_to_upload_files: DF.Check

View file

@ -16,16 +16,20 @@ class TestTranslation(IntegrationTestCase):
clear_cache()
def test_doctype(self):
translation_data = get_translation_data()
for lang, (source_string, new_translation) in translation_data.items():
doctype = "Translation"
meta = frappe.get_meta(doctype)
source_string = meta.get_label("translated_text")
for lang in ["de", "bs", "zh", "hr", "en", "sv"]:
frappe.local.lang = lang
original_translation = _(source_string)
original_translation = _(source_string, context=doctype)
new_translation = f"{original_translation} Customized"
docname = create_translation(lang, source_string, new_translation)
self.assertEqual(_(source_string), new_translation)
docname = create_translation(lang, source_string, new_translation, context=doctype)
self.assertEqual(_(source_string, context=doctype), new_translation)
frappe.delete_doc("Translation", docname)
self.assertEqual(_(source_string), original_translation)
frappe.delete_doc(doctype, docname)
self.assertEqual(_(source_string, context=doctype), original_translation)
def test_parent_language(self):
data = {
@ -60,37 +64,54 @@ class TestTranslation(IntegrationTestCase):
source = "User"
self.assertNotEqual(_(source, lang="de"), _(source, lang="es"))
def test_html_content_data_translation(self):
# ruff: noqa: RUF001
def test_html_content_translation(self):
source = """
<span style="color: rgb(51, 51, 51); font-family: &quot;Amazon Ember&quot;, Arial, sans-serif; font-size:
small;">MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to
your evening commute, you can work unplugged. When its time to kick back and relax,
you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time,
you can go away for weeks and pick up where you left off.Whatever the task,
fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.</span><br>
"""
To add dynamic subject, use jinja tags like
<div><pre><code>{{ doc.name }} Billed</code></pre></div>
""".strip()
target = """
MacBook Air dura hasta 12 horas increíbles entre cargas. Por lo tanto,
desde el café de la mañana hasta el viaje nocturno, puede trabajar desconectado.
Cuando es hora de descansar y relajarse, puede obtener hasta 12 horas de reproducción de películas de iTunes.
Y con hasta 30 días de tiempo de espera, puede irse por semanas y continuar donde lo dejó. Sea cual sea la tarea,
los procesadores Intel Core i5 e i7 de quinta generación con Intel HD Graphics 6000 son capaces de hacerlo.
"""
Um einen dynamischen Betreff hinzuzufügen, verwenden Sie Jinja-Tags wie
<div><pre><code>{{ doc.name }} Abgerechnet</code></pre></div>
""".strip()
create_translation("es", source, target)
frappe.local.lang = "de"
source = """
<span style="font-family: &quot;Amazon Ember&quot;, Arial, sans-serif; font-size:
small; color: rgb(51, 51, 51);">MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to
your evening commute, you can work unplugged. When its time to kick back and relax,
you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time,
you can go away for weeks and pick up where you left off.Whatever the task,
fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.</span><br>
"""
self.assertEqual(_(source), source)
self.assertTrue(_(source), target)
create_translation("de", source, target)
self.assertEqual(_(source), target)
def test_translated_html_is_sanitized(self):
source = "Translation with HTML"
target = """
<span style="color:red" onclick="alert('xss')">Hallo</span>
<script>alert("xss")</script>
<iframe src="https://example.com"></iframe>
<div>Ok</div>
""".strip()
docname = create_translation("de", source, target)
translated_text = frappe.db.get_value("Translation", docname, "translated_text")
self.assertIn('<span style="color:red">Hallo</span>', translated_text)
self.assertIn("<div>Ok</div>", translated_text)
self.assertNotIn("onclick", translated_text)
self.assertNotIn("<script", translated_text)
self.assertNotIn('alert("xss")', translated_text)
self.assertNotIn("<iframe", translated_text)
self.assertNotIn("example.com", translated_text)
frappe.local.lang = "de"
self.assertEqual(_(source), translated_text)
def test_plain_text_translation_with_angle_brackets_is_unchanged(self):
source = "Comparison"
target = "1 < 2 and 3 > 2"
docname = create_translation("de", source, target)
self.assertEqual(frappe.db.get_value("Translation", docname, "translated_text"), target)
def test_html_message_translations(self):
"""Test fallback for messages w/ HTML Tags"""
@ -100,27 +121,12 @@ class TestTranslation(IntegrationTestCase):
self.assertEqual(_(message, lang="zh"), translated_message)
def get_translation_data():
html_source_data = """<font color="#848484" face="arial, tahoma, verdana, sans-serif">
<span style="font-size: 11px; line-height: 16.9px;">Test Data</span></font>"""
html_translated_data = """<font color="#848484" face="arial, tahoma, verdana, sans-serif">
<span style="font-size: 11px; line-height: 16.9px;"> testituloksia </span></font>"""
return {
"hr": ["Test data", "Testdaten"],
"ms": ["Test Data", "ujian Data"],
"et": ["Test Data", "testandmed"],
"es": ["Test Data", "datos de prueba"],
"en": ["Quotation", "Tax Invoice"],
"fi": [html_source_data, html_translated_data],
}
def create_translation(lang, source_string, new_translation) -> str:
def create_translation(lang, source_string, new_translation, context=None) -> str:
doc = frappe.new_doc("Translation")
doc.language = lang
doc.source_text = source_string
doc.translated_text = new_translation
doc.context = context
doc.save()
return doc.name

View file

@ -1,12 +1,10 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe.model.document import Document
from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY
from frappe.utils import is_html, strip_html_tags
from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, change_translation_version
from frappe.utils import sanitize_html
class Translation(Document):
@ -28,11 +26,7 @@ class Translation(Document):
# end: auto-generated types
def validate(self):
if is_html(self.source_text):
self.remove_html_from_source()
def remove_html_from_source(self):
self.source_text = strip_html_tags(self.source_text).strip()
self.translated_text = sanitize_html(self.translated_text)
def on_update(self):
clear_user_translation_cache(self.language)
@ -46,3 +40,4 @@ class Translation(Document):
def clear_user_translation_cache(lang):
frappe.cache.hdel(USER_TRANSLATION_KEY, lang)
frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang)
change_translation_version()

View file

@ -42,7 +42,7 @@ class TestUser(IntegrationTestCase):
@staticmethod
def reset_password(user) -> str:
link = user.reset_password()
link = user._reset_password()
return parse_qs(urlparse(link).query)["key"][0]
def test_user_type(self):
@ -292,7 +292,7 @@ class TestUser(IntegrationTestCase):
c = FrappeClient(url)
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
self.assertEqual(res1.status_code, 404)
self.assertEqual(res1.status_code, 200)
self.assertEqual(res2.status_code, 429)
def test_user_rename(self):
@ -415,6 +415,12 @@ class TestUser(IntegrationTestCase):
# test API endpoint
with patch.object(user_module.frappe, "sendmail") as sendmail:
from unittest.mock import MagicMock
mock_q = MagicMock()
mock_q.name = "test-email-queue-name"
mock_q.message = "Subject: Test\n\nDear User, here is your link"
sendmail.return_value = mock_q
frappe.clear_messages()
test_user = frappe.get_doc("User", "test2@example.com")
self.assertEqual(reset_password(user="test2@example.com"), None)
@ -425,15 +431,28 @@ class TestUser(IntegrationTestCase):
update_password(old_password, old_password=new_password)
self.assertEqual(
frappe.message_log[0].get("message"),
f"Password reset instructions have been sent to {test_user.full_name}'s email",
"If this email is registered with us, we have sent password reset instructions to it. Please check your inbox.",
)
sendmail.assert_called_once()
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
self.assertEqual(reset_password(user="test2@example.com"), None)
self.assertEqual(reset_password(user="Administrator"), "not allowed")
self.assertEqual(reset_password(user="random"), "not found")
# Constant-response guarantee: every path — existing user, Administrator,
# and non-existent user — must return None AND enqueue the same generic
# message, so callers cannot distinguish between them.
_GENERIC_MSG = "If this email is registered with us, we have sent password reset instructions to it. Please check your inbox."
frappe.clear_messages()
self.assertIsNone(reset_password(user="test2@example.com"))
self.assertEqual(frappe.message_log[0].get("message"), _GENERIC_MSG)
frappe.clear_messages()
self.assertIsNone(reset_password(user="Administrator"))
self.assertEqual(frappe.message_log[0].get("message"), _GENERIC_MSG)
frappe.clear_messages()
self.assertIsNone(reset_password(user="random"))
self.assertEqual(frappe.message_log[0].get("message"), _GENERIC_MSG)
def test_user_onload_modules(self):
from frappe.desk.form.load import getdoc
@ -447,6 +466,21 @@ class TestUser(IntegrationTestCase):
sorted(m.get("module_name") for m in get_modules_from_all_apps()),
)
def test_default_app(self):
from frappe.apps import get_default_path
with test_user(roles=["System Manager"]) as user:
user.default_app = "next_erp"
user.save()
self.assertFalse(user.default_app)
frappe.set_user(user.name)
user.db_set("default_app", "next_erp")
user.reload()
self.assertTrue(user.default_app)
get_default_path() # defaults will also trigger hooks logic
@IntegrationTestCase.change_settings("System Settings", reset_password_link_expiry_duration=1)
def test_reset_password_link_expiry(self):
new_password = "new_password"

View file

@ -3,7 +3,7 @@ frappe.ui.form.on("User", {
frm.set_query("default_workspace", () => {
return {
filters: {
for_user: ["in", [null, frappe.session.user]],
for_user: ["in", ["", frappe.session.user]],
title: ["!=", "Welcome Workspace"],
},
};
@ -69,6 +69,8 @@ frappe.ui.form.on("User", {
frm.roles_editor.reset();
}
frm.fields_dict.new_password?.$input?.attr("autocomplete", "new-password");
if (
frm.can_edit_roles &&
!frm.is_new() &&
@ -108,7 +110,7 @@ frappe.ui.form.on("User", {
frappe.xcall("frappe.apps.get_apps").then((r) => {
let apps = r?.map((r) => r.name) || [];
frm.set_df_property("default_app", "options", [" ", ...apps]);
frm.set_df_property("default_app", "options", ["", ...apps]);
});
if (frm.is_new()) {

View file

@ -855,7 +855,7 @@
"options": "User Session Display"
},
{
"default": "0",
"default": "1",
"fieldname": "form_navigation_buttons",
"fieldtype": "Check",
"label": "Show navigation buttons"
@ -924,8 +924,8 @@
}
],
"make_attachments_public": 1,
"modified": "2026-03-24 21:30:57.199337",
"modified_by": "admin@seitimegames.com",
"modified": "2026-04-28 21:59:59.160099",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
"owner": "Administrator",

View file

@ -1,9 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import re
from collections.abc import Iterable
from datetime import timedelta
from functools import cached_property
from functools import cached_property, lru_cache
from typing import Any
import frappe
@ -233,6 +234,7 @@ class User(Document):
self.check_enable_disable()
self.ensure_unique_roles()
self.ensure_unique_role_profiles()
self.sync_role_profile_name()
self.remove_all_roles_for_guest()
self.validate_username()
self.remove_disabled_roles()
@ -248,6 +250,9 @@ class User(Document):
if self.language == "Loading...":
self.language = None
if self.default_app and self.default_app not in frappe.get_installed_apps():
self.default_app = ""
if (self.name not in ["Administrator", "Guest"]) and (not self.get_social_login_userid("frappe")):
self.set_social_login_userid("frappe", frappe.generate_hash(length=39))
@ -278,11 +283,11 @@ class User(Document):
def move_role_profile_name_to_role_profiles(self):
"""This handles old role_profile_name field if programatically set.
This behaviour will be remoed in future versions."""
This behaviour will be removed in future versions."""
if not self.role_profile_name:
return
current_role_profiles = [r.role_profile for r in self.role_profiles]
current_role_profiles = {r.role_profile for r in self.role_profiles}
if self.role_profile_name in current_role_profiles:
self.role_profile_name = None
return
@ -297,6 +302,10 @@ class User(Document):
self.append("role_profiles", {"role_profile": self.role_profile_name})
self.role_profile_name = None
def sync_role_profile_name(self):
"""Keep deprecated role_profile_name in sync for list view display."""
self.role_profile_name = self.role_profiles[0].role_profile if self.role_profiles else None
def validate_allowed_modules(self):
if self.module_profile:
module_profile = frappe.get_doc("Module Profile", self.module_profile)
@ -360,7 +369,7 @@ class User(Document):
def clean_name(self):
for field in ("first_name", "middle_name", "last_name"):
if field_value := self.get(field):
self.set(field, sanitize_html(field_value, always_sanitize=True))
self.set(field, sanitize_html(field_value, always_sanitize=True, disallowed_tags="*"))
def set_full_name(self):
self.full_name = " ".join(p for p in [self.first_name, self.middle_name, self.last_name] if p)
@ -378,9 +387,23 @@ class User(Document):
toggle_notifications(self.name, enable=cint(self.enabled), ignore_permissions=True)
self.disable_email_fields_if_user_disabled()
def email_new_password(self, new_password=None):
def set_new_password(self, new_password=None):
"""Set New Password for user"""
if new_password and not self.flags.in_insert:
_update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions)
outgoing_email_exists = frappe.db.exists(
"Email Account", {"default_outgoing": 1, "awaiting_password": 0}
)
if outgoing_email_exists:
email_message = _(
"Your password has been changed and you might have been logged out of all systems.<br>Please contact the Administrator for further assistance."
)
user_email = frappe.db.get_value("User", self.name, "email")
frappe.sendmail(
recipients=[user_email],
subject=_("Security Alert: Your password has been changed."),
content=email_message,
)
def set_system_user(self):
"""For the standard users like admin and guest, the user type is fixed."""
@ -435,7 +458,8 @@ class User(Document):
def send_password_notification(self, new_password):
try:
if self.flags.in_insert:
if self.name not in STANDARD_USERS:
if self.name in STANDARD_USERS:
return
if new_password:
# new password given, no email required
_update_password(
@ -453,7 +477,7 @@ class User(Document):
msgprint(_("Welcome email sent"))
return
else:
self.email_new_password(new_password)
self.set_new_password(new_password)
except frappe.OutgoingEmailError:
frappe.clear_last_message()
@ -467,7 +491,7 @@ class User(Document):
def validate_reset_password(self):
pass
def reset_password(self, send_email=False, password_expired=False):
def _reset_password(self, send_email=False, password_expired=False):
from frappe.utils import get_url
key = frappe.generate_hash()
@ -492,18 +516,24 @@ class User(Document):
def password_reset_mail(self, link):
reset_password_template = frappe.db.get_system_setting("reset_password_template")
self.send_login_mail(
q = self.send_login_mail(
_("Password Reset"),
"password_reset",
{"link": link},
now=True,
custom_template=reset_password_template,
)
if q:
raw_message = q.message
parts = re.split(r"(?i)Dear", raw_message, maxsplit=1)
if len(parts) > 1:
redacted_message = parts[0] + "[THE FOLLOWING CONTENT HAS BEEN REDACTED FOR SECURITY REASONS]"
frappe.db.set_value("Email Queue", q.name, "message", redacted_message, update_modified=False)
def send_welcome_mail_to_user(self):
from frappe.utils import get_url
link = self.reset_password()
link = self._reset_password()
subject = None
method = frappe.get_hooks("welcome_email")
if method:
@ -517,7 +547,7 @@ class User(Document):
welcome_email_template = frappe.db.get_system_setting("welcome_email_template")
self.send_login_mail(
q = self.send_login_mail(
subject,
"new_user",
dict(
@ -526,6 +556,12 @@ class User(Document):
),
custom_template=welcome_email_template,
)
if q:
raw_message = q.message
parts = re.split(r"(?i)Hello", raw_message, maxsplit=1)
if len(parts) > 1:
redacted_message = parts[0] + "[THE FOLLOWING CONTENT HAS BEEN REDACTED FOR SECURITY REASONS]"
frappe.db.set_value("Email Queue", q.name, "message", redacted_message, update_modified=False)
def send_login_mail(self, subject, template, add_args, now=None, custom_template=None):
"""send mail with login details"""
@ -557,7 +593,7 @@ class User(Document):
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
return frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
@ -619,18 +655,16 @@ class User(Document):
frappe.db.delete("List Filter", {"for_user": self.name})
# Remove user from Note's Seen By table
seen_notes = frappe.get_all("Note", filters=[["Note Seen By", "user", "=", self.name]], pluck="name")
for note_id in seen_notes:
note = frappe.get_doc("Note", note_id)
seen_notes = frappe.get_docs("Note", filters=[["Note Seen By", "user", "=", self.name]])
for note in seen_notes:
for row in note.seen_by:
if row.user == self.name:
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)
invites = frappe.get_docs("User Invitation", filters={"email": self.name})
for invite_doc in invites:
invite_doc.user = None
invite_doc.save(ignore_permissions=True)
@ -865,9 +899,14 @@ class User(Document):
@frappe.whitelist()
def get_timezones():
import zoneinfo
return {"timezones": _get_timezones()}
return {"timezones": zoneinfo.available_timezones()}
@lru_cache(maxsize=1)
def _get_timezones():
import pytz
return sorted(pytz.common_timezones)
@frappe.whitelist()
@ -1009,6 +1048,9 @@ def has_email_account(email: str):
@frappe.whitelist(allow_guest=False)
def get_email_awaiting(user: str):
if user != frappe.session.user:
frappe.has_permission("User", "read", doc=user, throw=True)
return frappe.get_all(
"User Email",
fields=["email_account", "email_id"],
@ -1128,25 +1170,32 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=get_password_reset_limit, seconds=60 * 60)
def reset_password(user: str) -> str:
def reset_password(user: str) -> None:
# Always return the same generic response regardless of whether the user
# exists, is disabled, or is restricted. This prevents username enumeration
# via different messages or HTTP status codes (CWE-204).
try:
user: User = frappe.get_doc("User", user)
if user.name == "Administrator":
return "not allowed"
if not user.enabled:
return "disabled"
user.validate_reset_password()
user.reset_password(send_email=True)
return frappe.msgprint(
msg=_("Password reset instructions have been sent to {}'s email").format(user.full_name),
title=_("Password Email Sent"),
)
user_doc: User = frappe.get_doc("User", user)
if user_doc.name != "Administrator" and user_doc.enabled:
user_doc.validate_reset_password()
user_doc._reset_password(send_email=True)
# For Administrator or disabled users: silently skip — same response below
except frappe.DoesNotExistError:
frappe.local.response["http_status_code"] = 404
frappe.clear_messages()
return "not found"
except frappe.OutgoingEmailError:
frappe.clear_messages()
frappe.log_error(title="Password reset email could not be sent", message=frappe.get_traceback())
except Exception:
frappe.clear_messages()
frappe.log_error(title="Password reset failed unexpectedly", message=frappe.get_traceback())
frappe.msgprint(
msg=_(
"If this email is registered with us, we have sent password reset instructions to it. Please check your inbox."
),
title=_("Password Reset"),
)
@frappe.whitelist()

View file

@ -4,6 +4,9 @@
frappe.listview_settings["User"] = {
add_fields: ["enabled", "user_type", "user_image"],
filters: [["enabled", "=", 1]],
onload(listview) {
this.set_default_app_options(listview);
},
prepare_data: function (data) {
data["user_for_avatar"] = data["name"];
},
@ -14,6 +17,15 @@ frappe.listview_settings["User"] = {
return [__("Disabled"), "grey", "enabled,=,0"];
}
},
set_default_app_options(listview) {
const default_app_field = frappe.meta.get_docfield("User", "default_app");
if (!default_app_field) return;
frappe.xcall("frappe.apps.get_apps").then((r) => {
let apps = r?.map((r) => r.name) || [];
default_app_field.options = ["", ...apps].join("\n");
});
},
};
frappe.help.youtube_id["User"] = "8Slw1hsTmUI";

View file

@ -39,9 +39,7 @@ class UserInvitation(Document):
self._after_insert()
def accept(self, ignore_permissions: bool = False):
accepted_now = self._accept()
if not accepted_now:
return
self._accept()
user, user_inserted = self._upsert_user(ignore_permissions)
self.save(ignore_permissions)
user.save(ignore_permissions)
@ -120,7 +118,7 @@ class UserInvitation(Document):
def _accept(self):
if self.status == "Accepted":
return False
frappe.throw(title=_("Error"), msg=_("Invitation already accepted"))
if self.status == "Expired":
frappe.throw(title=_("Error"), msg=_("Invitation is expired"))
if self.status == "Cancelled":
@ -128,6 +126,7 @@ class UserInvitation(Document):
self.status = "Accepted"
self.accepted_at = frappe.utils.now()
self.user = self.email
self.key = None
return True
def _upsert_user(self, ignore_permissions: bool = False):
@ -206,12 +205,11 @@ class UserInvitation(Document):
def mark_expired_invitations() -> None:
days = 3
invitations_to_expire = frappe.db.get_all(
invitations_to_expire = frappe.get_docs(
"User Invitation",
filters={"status": "Pending", "creation": ["<", frappe.utils.add_days(frappe.utils.now(), -days)]},
)
for invitation in invitations_to_expire:
invitation = frappe.get_doc("User Invitation", invitation.name)
invitation.expire()
# to avoid losing work in case the job times out without finishing
frappe.db.commit() # nosemgrep

View file

@ -51,9 +51,6 @@ def create_user_type(user_type):
if frappe.db.exists("User Type", user_type):
frappe.delete_doc("User Type", user_type)
user_type_limit = {frappe.scrub(user_type): 1}
update_site_config("user_type_doctype_limit", user_type_limit)
doc = frappe.get_doc(
{
"doctype": "User Type",

View file

@ -48,7 +48,6 @@ class UserType(Document):
if self.is_standard:
return
self.validate_document_type_limit()
self.validate_role()
self.add_role_permissions_for_user_doctypes()
self.add_role_permissions_for_select_doctypes()
@ -75,37 +74,6 @@ class UserType(Document):
for module in modules:
self.append("user_type_modules", {"module": module})
def validate_document_type_limit(self):
limit = frappe.conf.get("user_type_doctype_limit", {}).get(frappe.scrub(self.name))
if not limit and frappe.session.user != "Administrator":
frappe.throw(
_("User does not have permission to create the new {0}").format(frappe.bold(_("User Type"))),
title=_("Permission Error"),
)
if limit is None:
frappe.msgprint(
_("The limit has not set for the user type {0} in the site config file.").format(
frappe.bold(self.name)
),
title=_("Set Limit"),
)
return
if self.user_doctypes and len(self.user_doctypes) > limit:
frappe.throw(
_("The total number of user document types limit has been crossed."),
title=_("User Document Types Limit Exceeded"),
)
custom_doctypes = [row.document_type for row in self.user_doctypes if row.is_custom]
if custom_doctypes and len(custom_doctypes) > 3:
frappe.throw(
_("You can only set the 3 custom doctypes in the Document Types table."),
title=_("Custom Document Types Limit Exceeded"),
)
def validate_role(self):
if not self.role:
frappe.throw(_("The field {0} is mandatory").format(frappe.bold(_("Role"))))

View file

@ -22,6 +22,7 @@ STANDARD_EXCLUSIONS = [
"*/node_modules/*",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
"*/.github/*",
]
# tested via commands' test suite
@ -46,6 +47,9 @@ FRAPPE_EXCLUSIONS = [
"*frappe/setup.py",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
"*/frappe/database/postgres/*",
"*/.github/helper/ci.py",
"*/frappe/database/sqlite/*",
*TESTED_VIA_CLI,
]
@ -78,7 +82,12 @@ class CodeCoverage:
if self.app == "frappe":
omit.extend(FRAPPE_EXCLUSIONS)
self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
self.coverage = Coverage(
source=[source_path],
omit=omit,
include=STANDARD_INCLUSIONS,
data_suffix=True,
)
self.coverage.start()
return self

View file

@ -443,3 +443,53 @@ def _update_fieldname_references(field: CustomField, old_fieldname: str, new_fie
"insert_after",
new_fieldname,
)
def delete_custom_fields(custom_fields: dict, bypass_hooks: bool = False):
"""
Delete custom fields from doctypes.
:param custom_fields: Dict mapping doctype to field names.
:param bypass_hooks: If `True`, fast raw delete (skips hooks (doc events like on_trash)).
Example:
```
delete_custom_fields({"Address": ["custom_a", "custom_b"]})
delete_custom_fields({"ToDo": [{"fieldname": "cf_1"}]}, bypass_hooks=True)
````
"""
for doctype, fields in custom_fields.items():
fieldnames = []
if isinstance(fields, (list, tuple, set)):
for field in fields:
if isinstance(field, str):
fieldnames.append(field)
elif isinstance(field, dict) and field.get("fieldname"):
fieldnames.append(field["fieldname"])
if not fieldnames:
continue
fieldnames = tuple(set(fieldnames))
if bypass_hooks:
frappe.db.delete(
"Custom Field",
{
"fieldname": ("in", fieldnames),
"dt": doctype,
},
)
frappe.clear_cache(doctype=doctype)
else:
custom_field_names = frappe.get_all(
"Custom Field",
filters={"fieldname": ("in", fieldnames), "dt": doctype},
pluck="name",
)
for custom_field_name in custom_field_names:
frappe.get_doc("Custom Field", custom_field_name).delete(ignore_permissions=True, force=True)

View file

@ -5,8 +5,10 @@ import frappe
from frappe.custom.doctype.custom_field.custom_field import (
create_custom_field,
create_custom_fields,
delete_custom_fields,
rename_fieldname,
)
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests import IntegrationTestCase
@ -183,3 +185,50 @@ class TestCustomField(IntegrationTestCase):
self.assertFalse(doc.get(old))
field.delete()
def test_delete_custom_fields(self):
doctype = "ToDo"
fields = [
{
"fieldname": f"test_delete_{frappe.generate_hash(length=5)}",
"fieldtype": "Data",
"insert_after": "status",
}
for _ in range(4)
]
fieldnames = [f["fieldname"] for f in fields]
create_custom_fields({doctype: fields})
# create property setters for fields deleted via safe path (hooks should clean these up)
for fieldname in fieldnames[:2]:
make_property_setter(doctype, fieldname, "hidden", "1", "Check")
def field_exists(fieldname):
return frappe.db.exists("Custom Field", {"fieldname": fieldname, "dt": doctype})
def property_setter_exists(fieldname):
return frappe.db.exists("Property Setter", {"doc_type": doctype, "field_name": fieldname})
for fieldname in fieldnames:
self.assertTrue(field_exists(fieldname))
for fieldname in fieldnames[:2]:
self.assertTrue(property_setter_exists(fieldname))
# 1
delete_custom_fields({doctype: [fieldnames[0], fieldnames[0]]})
self.assertFalse(field_exists(fieldnames[0]))
self.assertFalse(property_setter_exists(fieldnames[0]))
# 2
delete_custom_fields({doctype: [{"fieldname": fieldnames[1]}]})
self.assertFalse(field_exists(fieldnames[1]))
self.assertFalse(property_setter_exists(fieldnames[1]))
# 3
delete_custom_fields({doctype: [fieldnames[2], fieldnames[2]]}, bypass_hooks=True)
self.assertFalse(field_exists(fieldnames[2]))
# 4
delete_custom_fields({doctype: [{"fieldname": fieldnames[3]}]}, bypass_hooks=True)
self.assertFalse(field_exists(fieldnames[3]))

View file

@ -24,6 +24,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
"allow_bulk_edit",
"queue_in_background",
"naming_section",
"naming_rule",
@ -222,6 +223,14 @@
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
},
{
"default": "1",
"depends_on": "istable",
"description": "Enable bulk edit for child table fields in Form view.",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
},
{
"depends_on": "email_append_to",
"fieldname": "subject_field",

View file

@ -13,13 +13,14 @@ import frappe.translate
from frappe import _
from frappe.core.doctype.doctype.doctype import (
check_email_append_to,
get_fields_not_allowed_in_list_view,
validate_autoincrement_autoname,
validate_fields_for_doctype,
validate_series,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter
from frappe.model import core_doctypes_list, no_value_fields
from frappe.model import core_doctypes_list
from frappe.model.docfield import supports_translation
from frappe.model.document import Document
from frappe.model.meta import trim_table
@ -41,6 +42,7 @@ class CustomizeForm(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_import: DF.Check
autoname: DF.Data | None
@ -319,12 +321,12 @@ class CustomizeForm(Document):
def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""):
if not self.allow_property_change(prop, meta_df, df):
if not self.allow_property_change(prop, meta_df, df, meta):
continue
self.make_property_setter(prop, df.get(prop), prop_type, fieldname=df.fieldname)
def allow_property_change(self, prop, meta_df, df):
def allow_property_change(self, prop, meta_df, df, meta):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
@ -360,8 +362,7 @@ class CustomizeForm(Document):
elif (
prop == "in_list_view"
and df.get(prop)
and df.fieldtype != "Attach Image"
and df.fieldtype in no_value_fields
and df.fieldtype in get_fields_not_allowed_in_list_view(meta)
):
frappe.msgprint(
_("'In List View' not allowed for type {0} in row {1}").format(df.fieldtype, df.idx)
@ -401,6 +402,10 @@ class CustomizeForm(Document):
elif prop == "in_global_search" and df.in_global_search != meta_df[0].get("in_global_search"):
self.flags.rebuild_doctype_for_global_search = True
elif prop == "is_virtual" and meta_df[0].get("is_virtual") == 0 and df.get("is_virtual") == 1:
frappe.msgprint(_("You can't set standard field {0} as virtual").format(frappe.bold(df.label)))
return False
return True
def set_property_setters_for_actions_and_links(self, meta):
@ -740,6 +745,7 @@ doctype_properties = {
"track_views": "Check",
"allow_auto_repeat": "Check",
"allow_import": "Check",
"allow_bulk_edit": "Check",
"show_name_in_global_search": "Check",
"show_preview_popup": "Check",
"default_email_template": "Data",
@ -787,6 +793,7 @@ docfield_properties = {
"print_hide": "Check",
"print_hide_if_no_value": "Check",
"report_hide": "Check",
"in_import_template": "Check",
"allow_on_submit": "Check",
"translatable": "Check",
"mandatory_depends_on": "Data",

View file

@ -48,6 +48,7 @@
"ignore_user_permissions",
"allow_on_submit",
"report_hide",
"in_import_template",
"remember_last_selected_value",
"hide_border",
"ignore_xss_filter",
@ -293,6 +294,13 @@
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"description": "Enable this option to include the field in the data import template",
"fieldname": "in_import_template",
"fieldtype": "Check",
"label": "Include in Import Template"
},
{
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
@ -523,7 +531,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-22 10:36:12.968197",
"modified": "2026-04-27 12:00:00.000000",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -81,6 +81,7 @@ class CustomizeFormField(Document):
ignore_xss_filter: DF.Check
in_filter: DF.Check
in_global_search: DF.Check
in_import_template: DF.Check
in_list_view: DF.Check
in_preview: DF.Check
in_standard_filter: DF.Check

View file

@ -111,6 +111,60 @@ def delete_property_setter(doc_type, property=None, field_name=None, row_name=No
if row_name:
filters["row_name"] = row_name
property_setters = frappe.db.get_values("Property Setter", filters)
_delete_property_setters(filters)
def bulk_delete_property_setters(property_setters: list[dict], bypass_hooks: bool = False):
"""
Delete property setters.
:param property_setters: List of filters for Property Setter rows.
:param bypass_hooks: If `True`, raw delete without doc hooks.
Example of `property_setters`:
```
[
{"doctype": "ToDo", "fieldname": "status", "property": "hidden"},
{"doctype": "ToDo", "fieldname": "status", "property": "read_only"},
]
```
---
Note: `doctype` and `fieldname` are mandatory.
"""
field_map = {
"doctype": "doc_type",
"fieldname": "field_name",
}
doctypes_to_clear = set()
for property_setter in property_setters:
filters = property_setter.copy()
for key, fieldname in field_map.items():
if key in filters:
filters[fieldname] = filters.pop(key)
if not filters:
continue
if not filters.get("doc_type") or not filters.get("field_name"):
frappe.throw(_("`doctype` and `fieldname` are required for deleting property setters."))
if bypass_hooks:
frappe.db.delete("Property Setter", filters)
doctypes_to_clear.add(filters["doc_type"])
else:
_delete_property_setters(filters)
for doctype in doctypes_to_clear:
frappe.clear_cache(doctype=doctype)
def _delete_property_setters(filters: dict):
property_setters = frappe.get_all("Property Setter", filters=filters, pluck="name")
for ps in property_setters:
frappe.get_doc("Property Setter", ps).delete(ignore_permissions=True, force=True)

View file

@ -1,7 +1,48 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe.custom.doctype.property_setter.property_setter import (
bulk_delete_property_setters,
)
from frappe.tests import IntegrationTestCase
class TestPropertySetter(IntegrationTestCase):
pass
def test_bulk_delete_property_setters(self):
doctype = "ToDo"
fieldname = "status"
property_1 = "hidden"
property_2 = "no_copy"
properties = [property_1, property_2]
for property_name in properties:
frappe.make_property_setter(
{
"doctype": doctype,
"fieldname": fieldname,
"property": property_name,
"value": 1,
"property_type": "Check",
}
)
def property_setter_exists(property_name):
return frappe.db.exists(
"Property Setter",
{"doc_type": doctype, "field_name": fieldname, "property": property_name},
)
for property_name in properties:
self.assertTrue(property_setter_exists(property_name))
# 1
bulk_delete_property_setters(
[{"doctype": doctype, "fieldname": fieldname, "property": property_1}],
bypass_hooks=True,
)
self.assertFalse(property_setter_exists(property_1))
# 2
bulk_delete_property_setters([{"doc_type": doctype, "field_name": fieldname, "property": property_2}])
self.assertFalse(property_setter_exists(property_2))

View file

@ -475,6 +475,9 @@ class Database:
if query_type in WRITE_QUERY_TYPES:
self.transaction_writes += 1
if frappe.conf.get("max_writes_per_transaction"):
self.MAX_WRITES_PER_TRANSACTION = cint(frappe.conf.max_writes_per_transaction)
if self.transaction_writes > self.MAX_WRITES_PER_TRANSACTION:
if self.auto_commit_on_many_writes:
self.commit()

View file

@ -8,7 +8,6 @@ import frappe
from frappe.database.utils import NestedSetHierarchy
from frappe.model.db_query import get_timespan_date_range
from frappe.query_builder import Field
from frappe.query_builder.functions import Coalesce
from frappe.utils import cstr
@ -48,6 +47,10 @@ def func_in(key: Field, value: list | tuple) -> frappe.qb:
"""
if isinstance(value, str):
value = value.split(",")
value = ["" if v is None else v for v in value]
if "" in value:
return key.isin(value) | key.isnull()
return key.isin(value)

View file

@ -84,7 +84,7 @@ def _apply_date_field_filter_conversion(value, operator: str, doctype: str, fiel
elif isinstance(value, datetime.datetime):
return value.date()
except AttributeError, TypeError, KeyError:
except (AttributeError, TypeError, KeyError):
pass
return value
@ -136,11 +136,7 @@ WORDS_PATTERN = re.compile(r"\w+")
COMMA_PATTERN = re.compile(r",\s*(?![^()]*\))")
# Pattern for validating simple field names (alphanumeric + underscore)
SIMPLE_FIELD_PATTERN = re.compile(r"^\w+$", flags=re.ASCII)
# Pattern for validating SQL identifiers (aliases, field names in functions)
# More restrictive: must start with letter or underscore
IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$", flags=re.ASCII)
SIMPLE_FIELD_PATTERN = re.compile(r"^\w+$")
# Pattern for detecting SQL function calls: identifier followed by opening parenthesis
FUNCTION_CALL_PATTERN = re.compile(r"^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\(", flags=re.ASCII)
@ -157,7 +153,7 @@ FUNCTION_CALL_PATTERN = re.compile(r"^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\(", flags=re.
# - ... as 'Child:field'
ALLOWED_FIELD_PATTERN = re.compile(
r"^(?:(`[\w\s-]+`|\w+)\.)?(`\w+`|\w+)(?:\s+as\s+(?:`[\w\s-]+`|'[\w\s:-]+'|\w+))?$",
flags=re.ASCII | re.IGNORECASE,
flags=re.IGNORECASE,
)
# Regex to parse field names:
@ -176,6 +172,9 @@ BACKTICK_FIELD_PARSE_REGEX = re.compile(r"^`tab([\w\s-]+)`\.(`?)(\w+)\2$")
# Group 3: Fieldname
CHILD_TABLE_FIELD_PATTERN = re.compile(r'^[`"]?tab([\w\s]+)[`"]?\.([`"]?)(\w+)\2$')
# Maximum value of an unsigned 64-bit integer
MAX_LIMIT = 18446744073709551615
# Direct mapping from uppercase function names to pypika function classes
FUNCTION_MAPPING = {
"COUNT": functions.Count,
@ -303,6 +302,11 @@ class Engine:
if offset:
if not isinstance(offset, int) or offset < 0:
frappe.throw(_("Offset must be a non-negative integer"), TypeError)
# In MariaDB and SQLite, offset requires limit
if not self.is_postgres and not limit:
self.query = self.query.limit(MAX_LIMIT)
self.query = self.query.offset(offset)
if distinct:
@ -676,7 +680,7 @@ class Engine:
else:
try:
fallback_value = int(fallback_sql)
except ValueError, TypeError:
except (ValueError, TypeError):
fallback_value = fallback_sql
return operator_fn(_field, ValueWrapper(fallback_value))
@ -705,7 +709,7 @@ class Engine:
else:
try:
fallback_value = int(fallback_sql)
except ValueError, TypeError:
except (ValueError, TypeError):
fallback_value = fallback_sql
if fallback_value == _value:
@ -2424,14 +2428,15 @@ class SQLFunctionParser:
).format(arg),
frappe.ValidationError,
)
elif self._is_valid_field_name(arg):
self._check_function_field_permission(arg)
return self.engine.table[arg]
# Check if it's a numeric string like "1" (for COUNT(1), etc.)
elif arg.isdigit():
return int(arg)
elif self._is_valid_field_name(arg):
self._check_function_field_permission(arg)
return self.engine.table[arg]
else:
frappe.throw(
_(
@ -2443,7 +2448,7 @@ class SQLFunctionParser:
def _is_valid_field_name(self, name: str) -> bool:
"""Check if a string is a valid field name."""
# Field names should only contain alphanumeric characters and underscores
return IDENTIFIER_PATTERN.match(name) is not None
return SIMPLE_FIELD_PATTERN.match(name) is not None
def _validate_alias(self, alias: str):
"""Validate alias name for SQL injection."""
@ -2456,7 +2461,7 @@ class SQLFunctionParser:
# Alias should be a simple identifier
# Note: pypika wraps aliases in backticks, so anything without backticks is safe
if not IDENTIFIER_PATTERN.match(alias):
if not SIMPLE_FIELD_PATTERN.match(alias):
frappe.throw(
_("Invalid alias format: {0}. Alias must be a simple identifier.").format(alias),
frappe.ValidationError,

View file

@ -5,7 +5,7 @@ from frappe import _
from frappe.utils import cint, cstr, flt
from frappe.utils.defaults import get_not_null_defaults
# This matches anything that isn't [a-zA-Z0-9_]
# This matches anything that isn't Unicode Word Characters, Numbers and Underscore.
SPECIAL_CHAR_PATTERN = re.compile(r"[\W]", flags=re.UNICODE)
VARCHAR_CAST_PATTERN = re.compile(r"varchar\(([\d]+)\)")

View file

@ -661,6 +661,9 @@ def update_onboarding_step(name: str | int, field: str, value: int | str):
"""
from frappe.utils.telemetry import capture
allowed_fields = ["is_skipped", "is_complete"]
if field not in allowed_fields:
return
frappe.db.set_value("Onboarding Step", name, field, value)
capture(frappe.scrub(name), app="frappe_onboarding", properties={field: value})
@ -682,6 +685,9 @@ def get_onboarding_data(module: str):
Return:
dict: onboarding data
"""
if not frappe.get_system_settings("enable_onboarding"):
return []
onboardings = []
onboarding_doc = frappe.get_doc("Module Onboarding", module)
if onboarding_doc.is_complete:

View file

@ -36,7 +36,7 @@
},
{
"bold": 1,
"description": "SQL Conditions. Example: status=\"Open\"",
"description": "SQL Conditions. Example: {\"status\" : \"open\", \"priority\" : \"medium\"}",
"fieldname": "condition",
"fieldtype": "Small Text",
"label": "Condition"
@ -52,7 +52,7 @@
],
"issingle": 1,
"links": [],
"modified": "2024-03-23 16:01:29.575802",
"modified": "2026-04-01 12:18:08.821282",
"modified_by": "Administrator",
"module": "Desk",
"name": "Bulk Update",
@ -70,6 +70,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],

View file

@ -31,17 +31,18 @@ class BulkUpdate(Document):
def bulk_update(self):
self.check_permission("write")
limit = self.limit if self.limit and cint(self.limit) < 500 else 500
condition = ""
query_args = {"doctype": self.document_type, "limit": limit, "pluck": "name"}
if self.condition:
if ";" in self.condition:
frappe.throw(_("; not allowed in condition"))
try:
filters = frappe.parse_json(self.condition)
if isinstance(filters, dict):
if "or_filters" in filters:
query_args["or_filters"] = filters.pop("or_filters")
query_args["filters"] = filters
except Exception as e:
frappe.throw(_("The Bulk Update could not happen due to <b>{0}</b>").format(str(e)))
condition = f" where {self.condition}"
docnames = frappe.db.sql_list(
f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0"""
)
docnames = frappe.get_all(**query_args)
return submit_cancel_or_update_docs(
self.document_type, docnames, "update", {self.field: self.update_value}
)

View file

@ -103,3 +103,45 @@ class TestBulkUpdate(IntegrationTestCase):
docnames_bg = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name")
submit_cancel_or_update_docs(self.doctype, docnames_bg, action="update", data=update_data)
self.wait_for_assertion(lambda: check_child_field(docnames_bg, "_Test Child Updated"))
def test_bulk_update_conditions(self):
"""Test the whitelisted bulk update method"""
todo_names = []
for i in range(5):
doc = frappe.get_doc(
{
"doctype": "ToDo",
"description": f"Bulk Update Status Test {i}",
"status": "Open" if i < 3 else "Closed",
}
).insert()
todo_names.append(doc.name)
try:
condition_json = frappe.as_json({"status": "Open", "name": ["in", todo_names]})
bulk_upd = frappe.get_doc(
{
"doctype": "Bulk Update",
"document_type": "ToDo",
"field": "status",
"update_value": "Closed",
"condition": condition_json,
"limit": 5,
}
)
bulk_upd.bulk_update()
updated_docs = frappe.get_all("ToDo", filters={"name": ["in", todo_names]}, fields=["status"])
for doc in updated_docs:
self.assertEqual(doc.status, "Closed")
remaining_open_count = frappe.db.count("ToDo", {"name": ["in", todo_names], "status": "Open"})
self.assertEqual(remaining_open_count, 0)
finally:
for name in todo_names:
frappe.delete_doc("ToDo", name)
frappe.db.commit()

View file

@ -4,6 +4,7 @@
import frappe
from frappe.model.document import Document
from frappe.query_builder.utils import DocType
from frappe.utils import has_common
class CustomHTMLBlock(Document):
@ -23,7 +24,12 @@ class CustomHTMLBlock(Document):
style: DF.Code | None
# end: auto-generated types
pass
def validate(self):
self.validate_private()
def validate_private(self):
if not has_common(frappe.get_roles(), ["Administrator", "System Manager", "Workspace Manager"]):
self.private = 1
@frappe.whitelist()

View file

@ -63,6 +63,7 @@ class DesktopIcon(Document):
clear_desktop_icons_cache(user=self.owner)
def after_rename(self, old, new, merge):
if self.standard and self.app:
delete_desktop_icon_file(self.app, old)
self.export_desktop_icon()

View file

@ -48,6 +48,8 @@ def save_layout(user: str, layout: str, new_icons: str | None = None):
new_workspace = frappe.new_doc("Workspace")
new_workspace.update(workspace)
new_workspace.title = new_workspace.label
if not new_workspace.public:
new_workspace.for_user = frappe.session.user
new_workspace.save()
return add_workspace_to_desktop(new_workspace.name)
desktop_icon = frappe.new_doc("Desktop Icon")

View file

@ -137,7 +137,7 @@ class Event(Document):
return
for participant in self.event_participants:
if communications := frappe.get_all(
if communications := frappe.get_docs(
"Communication",
filters=[
["Communication", "reference_doctype", "=", self.doctype],
@ -145,11 +145,9 @@ class Event(Document):
["Communication Link", "link_doctype", "=", participant.reference_doctype],
["Communication Link", "link_name", "=", participant.reference_docname],
],
pluck="name",
distinct=True,
):
for comm in communications:
communication = frappe.get_doc("Communication", comm)
for communication in communications:
self.update_communication(participant, communication)
else:
meta = frappe.get_meta(participant.reference_doctype)
@ -238,8 +236,15 @@ class Event(Document):
@frappe.whitelist()
def update_attending_status(event_name: str, attendee: str, status: str):
event_doc = frappe.get_doc("Event", event_name)
caller = frappe.session.user
if event_doc.owner == attendee == frappe.session.user:
if attendee != caller:
if event_doc.owner != caller and not frappe.has_permission("Event", "write", event_name):
frappe.throw(
_("You are not allowed to update attendance for another user."), frappe.PermissionError
)
if event_doc.owner == caller:
frappe.db.set_value("Event", event_name, "attending", status)
return
@ -248,8 +253,7 @@ def update_attending_status(event_name: str, attendee: str, status: str):
frappe.db.set_value("Event Participants", participant.name, "attending", status)
return
if not has_permission(event_doc, user=attendee):
frappe.throw(_("You are not allowed to update the status of this event."))
frappe.throw(_("Attendee not found in this event."))
@frappe.whitelist()
@ -339,7 +343,12 @@ def get_events(
for_reminder: bool = False,
filters: str | list | dict[str, Any] | None = None,
) -> list[frappe._dict]:
user = user or frappe.session.user
caller = frappe.session.user
target_user = user or caller
if user and user != caller:
if not frappe.has_permission("Event", ptype="read"):
frappe.throw(_("You are not allowed to view events for another user."), frappe.PermissionError)
type EventLikeDict = Event | frappe._dict
resolved_events: list[EventLikeDict] = []
@ -411,7 +420,7 @@ def get_events(
{
"start": start,
"end": end,
"user": user,
"user": target_user,
},
as_dict=True,
)

View file

@ -76,6 +76,7 @@ class FormTour(Document):
@frappe.whitelist()
def reset_tour(tour_name: str):
frappe.only_for("System Manager")
for user in frappe.get_all("User", pluck="name"):
onboarding_status = frappe.parse_json(frappe.db.get_value("User", user, "onboarding_status"))
onboarding_status.pop(tour_name, None)

View file

@ -36,6 +36,7 @@ class Note(Document):
if not self.content:
self.content = "<span></span>"
self.content = frappe.utils.sanitize_html(self.content, always_sanitize=True)
def before_print(self, settings=None):
self.print_heading = self.name

View file

@ -504,7 +504,7 @@ frappe.ui.form.on("Number Card", {
<td class="text-center">
<a class="remove-filter text-muted" style="cursor: pointer;">
<svg class="icon icon-sm">
<use href="#icon-close" class="close"></use>
<use href="#icon-x" class="close"></use>
</svg>
</a>
</td>

View file

@ -29,9 +29,7 @@ class SystemConsole(Document):
try:
frappe.local.debug_log = []
if self.type == "Python":
safe_exec(
self.console, script_filename="System Console", restrict_commit_rollback=not self.commit
)
safe_exec(self.console, script_filename="System Console")
self.output = "\n".join(frappe.debug_log)
elif self.type == "SQL":
frappe.db.begin(read_only=True)

View file

@ -11,8 +11,8 @@ frappe.ui.form.on("Workspace", {
frm.trigger("add_to_desktop");
let url = `/desk/${
frm.doc.public
? frappe.router.slug(frm.doc.title)
: "private/" + frappe.router.slug(frm.doc.title)
? frappe.router.slug(frm.doc.name)
: "private/" + frappe.router.slug(frm.doc.name)
}`;
frm.sidebar
.add_user_action(__("Go to Workspace"))

View file

@ -76,18 +76,6 @@ class Workspace(Document):
if self.public and not is_workspace_manager() and not disable_saving_as_public():
frappe.throw(_("You need to be Workspace Manager to edit this document"))
if (
not self.public
and self.for_user
and self.for_user != frappe.session.user
and not is_workspace_manager()
):
frappe.throw(
_("You are not allowed to edit this workspace"),
frappe.PermissionError,
)
if self.has_value_changed("title"):
validate_route_conflict(self.doctype, self.title)
else:

View file

@ -16,6 +16,7 @@
"child",
"navigate_to_tab",
"url",
"open_in_new_tab",
"display_section",
"collapsible_column",
"collapsible",
@ -168,13 +169,20 @@
"fieldname": "filter_area",
"fieldtype": "HTML",
"label": "Filter Area"
},
{
"default": "1",
"depends_on": "eval:doc.link_type === \"URL\";",
"fieldname": "open_in_new_tab",
"fieldtype": "Check",
"label": "Open in New Tab"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-12 15:35:56.930873",
"modified": "2026-03-15 02:26:37.285903",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Sidebar Item",

View file

@ -24,6 +24,7 @@ class WorkspaceSidebarItem(Document):
link_to: DF.DynamicLink | None
link_type: DF.Literal["DocType", "Page", "Report", "Workspace", "Dashboard", "URL"]
navigate_to_tab: DF.Autocomplete | None
open_in_new_tab: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View file

@ -58,6 +58,9 @@ def follow_document(doctype: str, doc_name: str, user: str) -> Document | bool:
frappe.toast(_("Administrator can't follow"))
return False
if user != frappe.session.user and not frappe.has_permission("Document Follow", "write"):
frappe.throw(_("You can only follow documents for yourself."), frappe.PermissionError)
if not frappe.db.get_value("User", user, "document_follow_notify", ignore=True, cache=True):
frappe.toast(_("Document follow is not enabled for this user."))
return False
@ -74,6 +77,9 @@ def follow_document(doctype: str, doc_name: str, user: str) -> Document | bool:
@frappe.whitelist()
def unfollow_document(doctype: str, doc_name: str, user: str) -> bool:
if user != frappe.session.user and not frappe.has_permission("Document Follow", "write"):
frappe.throw(_("You can only unfollow documents for yourself."), frappe.PermissionError)
doc = frappe.get_all(
"Document Follow",
filters={"ref_doctype": doctype, "ref_docname": doc_name, "user": user},

View file

@ -64,6 +64,11 @@ def cancel(
if workflow_state_fieldname and workflow_state:
doc.set(workflow_state_fieldname, workflow_state)
if doc.meta.queue_in_background and not is_scheduler_inactive():
queue_submission(doc, "Cancel")
return
doc.cancel()
send_updated_docs(doc)
frappe.msgprint(frappe._("Cancelled"), indicator="red", alert=True)

Some files were not shown because too many files have changed in this diff Show more