Compare commits

...

157 Commits

Author SHA1 Message Date
Nikhil Kothari
574c86729b fix: type hints for get_children and enqueue opening stock after commit (semgrep) 2026-09-25 16:10:41 +05:30
Nishka Gosalia
ead172c4e5 Merge pull request #59228 from nishkagosalia/work-order-ux-cleanup
fix(UX): Work order UX cleanup

(cherry picked from commit f45b22b79c)
2026-09-25 15:47:02 +05:30
Nishka Gosalia
281eb112b9 Merge pull request #59329 from nishkagosalia/job-card-ux-cleanup
fix(UX): Job card cleanup

(cherry picked from commit 9a79253ee1)
2026-09-25 15:45:38 +05:30
Nikhil Kothari
62ccd0cefb feat: update icons and documentation links for DocTypes and reports (#59235)
* feat: update icons and documentation links for DocTypes

* feat: add documentation links in ERPNext

* fix: migrate to newer Lucide icons

(cherry picked from commit 3c251254b8)
2026-09-25 15:40:47 +05:30
Mihir Kandoi
63e419b4c6 chore(desk): hide CRM and Support on the dock (#59208)
(cherry picked from commit dd72c2688b)
2026-09-25 15:37:53 +05:30
Khushi Rawat
7d7eb9b151 Merge pull request #59085 from khushi8112/print-format-classic-item-description
fix: show item description in Classic print formats
(cherry picked from commit 4974372073)
2026-09-25 15:37:53 +05:30
Mihir Kandoi
fc8994f746 fix(ui): do not decorate self-referential links with the row's own title (#59023)
(cherry picked from commit 41ad83dfb1)
2026-09-25 15:35:59 +05:30
Soham Kulkarni
e4692a4acb Merge pull request #58729 from sokumon/sidebar-changes
fix: more sidebar improvements
(cherry picked from commit f8951034be)
2026-09-25 15:35:59 +05:30
Sumit Jain
90e6831887 Merge pull request #58726 from frappe/fix-account-tree
feat(accounts): disabled marker and cost center parity in tree views

(cherry picked from commit 3b9535ed91)
2026-09-25 15:34:03 +05:30
Sumit Jain
7bdb0ef4bb fix: enhance tree view functionality for accounts and cost centers (#58520)
(cherry picked from commit 31761699ae)
2026-09-25 15:33:34 +05:30
Soham Kulkarni
2c0eaa4b6b Merge pull request #58476 from sokumon/module-sidebars
feat: erpnext sidebars and workspaces
(cherry picked from commit 64186f229f)
2026-09-25 15:33:34 +05:30
Khushi Rawat
7c7f69948d Merge pull request #57838 from khushi8112/hide/redundant-primary-address-contact-fields
refactor(ui): hide the primary address and contact preview fields

(cherry picked from commit 0b01b9bcd3)
2026-09-25 15:32:02 +05:30
Khushi Rawat
0fd8d380ce Merge pull request #58096 from khushi8112/refactor/item-prices-embedded-list
refactor: use EmbeddedList for item prices
(cherry picked from commit 7212e6d989)
2026-09-25 15:30:30 +05:30
Nishka Gosalia
82176cf7c8 Merge pull request #57845 from nishkagosalia/settings-mapping-cleanup
chore: restructure exported files of doctype settings
(cherry picked from commit d9e048ece7)
2026-09-25 15:29:19 +05:30
Soham Kulkarni
bfd1a6f022 Merge pull request #57614 from sokumon/item-default
fix: unchecking default workspace
(cherry picked from commit ec02b5fa64)
2026-09-25 15:29:19 +05:30
Khushi Rawat
1ff926da30 Merge pull request #57520 from khushi8112/feature/default-modern-print-formats
feat: default new sites to the Modern with Images print formats
(cherry picked from commit 4f1adb8a94)
2026-09-25 15:29:19 +05:30
Khushi Rawat
e83908173e Merge pull request #57451 from khushi8112/feature/request-for-quotation-print-formats
feat: add four Request for Quotation print formats built with the print format builder
(cherry picked from commit 56a9ca334b)
2026-09-25 15:29:19 +05:30
Khushi Rawat
de6d2e125e Merge pull request #57450 from khushi8112/feature/quotation-print-formats
feat: add four Quotation print formats built with the print format builder
(cherry picked from commit aacccfb958)
2026-09-25 15:29:19 +05:30
Khushi Rawat
989579a900 Merge pull request #57449 from khushi8112/feature/pos-invoice-print-formats
feat: add four POS Invoice print formats built with the print format builder
(cherry picked from commit bd5d6fb9d8)
2026-09-25 15:29:19 +05:30
Khushi Rawat
8649c908b8 Merge pull request #57447 from khushi8112/feature/purchase-invoice-print-formats
feat: add four Purchase Invoice print formats built with the print format builder
(cherry picked from commit 22334def56)
2026-09-25 15:29:18 +05:30
Khushi Rawat
3125ac3d47 Merge pull request #57446 from khushi8112/feature/purchase-order-print-formats
feat: add four Purchase Order print formats built with the print format builder
(cherry picked from commit 5631acc39b)
2026-09-25 15:29:18 +05:30
Khushi Rawat
412ae07aba Merge pull request #57445 from khushi8112/feature/delivery-note-print-formats
feat: add four Delivery Note print formats built with the print format builder
(cherry picked from commit dd0f763613)
2026-09-25 15:29:18 +05:30
Khushi Rawat
126a04bbbb Merge pull request #57437 from khushi8112/feature/sales-order-print-formats
feat: add four Sales Order print formats built with the print format builder
(cherry picked from commit b71a9b8273)
2026-09-25 15:29:18 +05:30
Khushi Rawat
280d43bf91 Merge pull request #57430 from khushi8112/feature/sales-invoice-print-formats
feat: add four Sales Invoice print formats
(cherry picked from commit 7a76c5f268)
2026-09-25 15:29:18 +05:30
Nishka Gosalia
89ad39c9af Merge pull request #57333 from nishkagosalia/fix-settings-map
fix: settings map cleanup
(cherry picked from commit eafd43769b)
2026-09-25 15:29:18 +05:30
Mihir Kandoi
2ee542a7ef Merge pull request #57225 from mihir-kandoi/fix-duplicate-workspace-links
fix: remove duplicate links from home and projects workspaces
(cherry picked from commit 0cba26c608)
2026-09-25 15:29:18 +05:30
Nishka Gosalia
418870812a feat: map settings for DocTypes to show on settings dialog (#57025)
fix: mapping settings for DocType settings
(cherry picked from commit 5fc03a116a)
2026-09-25 15:29:18 +05:30
Soham Kulkarni
bb84f54941 Merge pull request #57134 from sokumon/merge-workspaces
fix: merge erpnext workspaces
(cherry picked from commit 4d2b603ba6)
2026-09-25 15:29:18 +05:30
rohitwaghchaure
0c60ff7838 fix: workspace for stock and manufacturing (#56906)
(cherry picked from commit 607f0e943f)
2026-09-25 15:29:18 +05:30
Nikhil Kothari
02f43d2b5f fix: replace all old icons (#56864)
(cherry picked from commit dc09362454)
2026-09-25 15:26:33 +05:30
Soham Kulkarni
889b6900dd Merge pull request #56656 from sokumon/revamp-workspaces
chore: exporting workspaces with sidebars
(cherry picked from commit 3eaea74a51)
2026-09-25 15:23:41 +05:30
Khushi Rawat
8ec1a8dd7b Merge pull request #54570 from khushi8112/item-opening-stock-dialog
feat: add opening stock dialog for stock items
(cherry picked from commit bda7a8ced2)
2026-09-25 15:23:40 +05:30
rohitwaghchaure
5b1e9b5ef1 fix: opening stock not working for serial / batch (#51781)
(cherry picked from commit 589a393b5c)
2026-09-25 15:23:40 +05:30
Nabin Hait
7812ca1f9f chore: remove leaderboard dead code (#55030)
(cherry picked from commit 6114293b92)
2026-09-25 15:20:03 +05:30
Deepesh Garg
869ed9dfce Merge pull request #52453 from deepeshgarg007/erpnext_form_cleanups_v1
fix: Currency fields label (Sales Invoice)
(cherry picked from commit 4584893542)
2026-09-25 15:18:06 +05:30
Sudharsanan Ashok
f3c74f4639 Merge pull request #59439 from frappe/mergify/bp/version-16-hotfix/pr-59430
fix(accounts): filter purchase register by supplier group from supplier master (backport #59430)
2026-09-25 15:02:00 +05:30
Sudharsanan Ashok
6686da2cce fix(accounts): filter purchase register by supplier group from supplier master (#59430)
(cherry picked from commit 75efa2d1cf)
2026-09-25 09:14:53 +00:00
Mihir Kandoi
4426228532 fix(manufacturing): scope secondary items to job card (backport #58260) (#59435) 2026-09-25 09:08:45 +00:00
Mihir Kandoi
223adf655f fix: count only matched batches on work order reservations (backport #59424) (#59427) 2026-09-25 08:39:05 +00:00
Mihir Kandoi
544ca623bd fix: release the work order reservation before previewing a stock entry (backport #59425) (#59428) 2026-09-25 08:18:58 +00:00
Deepesh Garg
30baa235e4 Merge pull request #59313 from frappe/mergify/bp/version-16-hotfix/pr-59276
feat: make bank reconciliation dialog voucher types extensible (#59276)
2026-09-25 10:59:33 +05:30
Deepesh Garg
3a7a7dbd58 test: cover the bank reconciliation voucher type registry
Node unit tests evaluate the dialog script with stubbed frappe globals and
cover preserving pre-registered types, filtering them per transaction,
injecting their dialog fields, the submit and full-page return contracts
and rejecting unregistered types. yarn test:js runs them and a js unit
tests job runs them on every PR.

Also restores the track_voucher call on the full-page path that the merge
with develop dropped, and registers the after-submit reconciliation hooks
for every registered voucher type.
2026-09-25 10:22:09 +05:30
Deepesh Garg
4fe41c925d feat: make bank reconciliation dialog voucher types extensible
Other apps can register additional "Create Voucher" document types in
erpnext.accounts.bank_reconciliation.voucher_types with their own dialog
fields, applicability check and creation call. Payment Entry and Journal
Entry now go through the same registry, which also removes the duplicated
create and edit-in-full-page call blocks.
2026-09-25 10:20:23 +05:30
mergify[bot]
032fcad61b fix: set correct transaction type on args copy while evaulating item wise pricing rule (backport #59406) (#59409)
fix: set correct transaction type on args copy while evaulating item wise pricing rule (#59406)

(cherry picked from commit 9cc2c2272a)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-09-25 06:32:57 +05:30
Shllokkk
80b26b2f28 Merge pull request #59389 from frappe/mergify/bp/version-16-hotfix/pr-59386
fix: skip activities fetch on unsaved CRM doc (backport #59386)
2026-09-24 18:18:07 +05:30
Shllokkk
ede9f11500 fix: skip activities fetch on unsaved CRM doc
(cherry picked from commit 87052020fb)
2026-09-24 12:41:44 +00:00
Sudharsanan Ashok
5306aaf53d fix(stock): restrict delivery to reserved serial nos when ensure delivery by serial no is set (#59381)
* fix(stock): restrict delivery to reserved serial nos when ensure delivery by serial no is set

delivery note and sales invoice with update stock now throw on submit if a sales order item with ensure delivery based on produced serial no has no serial reservation, or if a delivered serial no is not one of the serial nos reserved for it. the check runs after the stock ledger is posted, so serial nos auto picked on submit are validated too.

* test(stock): add tests for delivery restricted to reserved produced serial nos

* fix(stock): reserve stock in work order for ensure delivery by serial no items

a new work order for a sales order item with ensure delivery based on produced serial no now reserves stock by default. the checkbox is hidden and cleared on the sales order when stock reservation is disabled in stock settings, and the delivery check is skipped in that case. the missing reservation error now asks to reserve the produced serial nos from the sales order, since reserve stock cannot be enabled on a work order after production.

* test(stock): add tests for work order reserve stock and ensure delivery checkbox clearing
2026-09-24 15:57:13 +05:30
Sudharsanan Ashok
3e851108f1 Merge pull request #59366 from frappe/mergify/bp/version-16-hotfix/pr-59168
fix(regional): report uae vat 201 sales vat in company currency  (backport #59168)
2026-09-24 10:58:35 +05:30
Sudharsanan11
e55efadd0d fix: resolve conflicts 2026-09-24 10:34:58 +05:30
Sudharsanan Ashok
2387c67952 fix(regional): report uae vat 201 sales vat in company currency (#59168)
(cherry picked from commit 6e0ec5af31)

# Conflicts:
#	erpnext/regional/report/uae_vat_201/uae_vat_201.py
2026-09-24 04:39:28 +00:00
Mihir Kandoi
0c02cbaf1e perf(stock): serve Item link search from an FTS5 trigram index (backport #59166) (#59350) 2026-09-24 08:39:16 +05:30
Mihir Kandoi
ad8f945cce fix: UOM whole number check truncated instead of rounding (backport #57861) (#59364) 2026-09-24 03:06:25 +00:00
Shllokkk
d0290ea98e Merge pull request #59353 from Shllokkk/mrp-po-missing-supplier-backport
fix: handle missing and item group default supplier in MRP make_order
2026-09-24 04:01:31 +05:30
Shllokkk
61f66ed579 test: cover missing and item-group-fallback supplier in MRP make_order 2026-09-24 03:47:36 +05:30
mergify[bot]
1e16c827ef fix(pos): reset stale selected mode of payment on new invoice (backport #59308) (#59355)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
2026-09-23 19:26:48 +00:00
Shllokkk
be283b70f2 fix: resolve MRP purchase supplier with item group fallback, else throw 2026-09-24 00:27:24 +05:30
Shllokkk
79858d5af6 fix(manufacturing): classify MRP rows by purchase item, not missing BOM (#58510) 2026-09-24 00:27:18 +05:30
Diptanil Saha
48635ee6b5 fix(permissions)!: add permission checks to whitelisted methods and the DocPerm rows they need (#59284) 2026-09-23 23:01:45 +05:30
Sudharsanan Ashok
8732c4eee3 Merge pull request #59339 from frappe/mergify/bp/version-16-hotfix/pr-59336
fix(selling): use preferred addresses in customer quotations (backport #59336)
2026-09-23 19:02:26 +05:30
Sudharsanan Ashok
0e1657a35a Merge pull request #59281 from frappe/mergify/bp/version-16-hotfix/pr-59277
fix(crm): keep opportunity type default untranslated (backport #59277)
2026-09-23 18:54:55 +05:30
pandiyan
c019bb5c28 fix(selling): use preferred addresses in customer quotations 2026-09-23 18:48:11 +05:30
Mihir Kandoi
1ead4cebcf fix(quality): block Quality Inspection submit without a sample size (backport #59330) (#59331) 2026-09-23 10:56:56 +00:00
mergify[bot]
242f4219cb fix(pos): quote the reversed row's rate on a consolidated credit note (backport #59320) (#59323)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-09-23 16:02:13 +05:30
Mihir Kandoi
5d495c2763 fix(manufacturing): finish a work order after partial material consumption (#59312) 2026-09-23 07:38:13 +00:00
Deepesh Garg
f1cdeab601 test: cover the bank reconciliation voucher type registry
Node unit tests evaluate the dialog script with stubbed frappe globals and
cover preserving pre-registered types, filtering them per transaction,
injecting their dialog fields, the submit and full-page return contracts
and rejecting unregistered types. yarn test:js runs them and a js unit
tests job runs them on every PR.

Also restores the track_voucher call on the full-page path that the merge
with develop dropped, and registers the after-submit reconciliation hooks
for every registered voucher type.

(cherry picked from commit 60f0a736ab)

# Conflicts:
#	erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
2026-09-23 07:19:23 +00:00
Deepesh Garg
a657adba37 feat: make bank reconciliation dialog voucher types extensible
Other apps can register additional "Create Voucher" document types in
erpnext.accounts.bank_reconciliation.voucher_types with their own dialog
fields, applicability check and creation call. Payment Entry and Journal
Entry now go through the same registry, which also removes the duplicated
create and edit-in-full-page call blocks.

(cherry picked from commit 96db1071da)

# Conflicts:
#	erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
2026-09-23 07:19:23 +00:00
Khushi Rawat
868de6e513 Merge pull request #59289 from frappe/mergify/bp/version-16-hotfix/pr-59262
fix(journal-entry): carry reference fields into reverse journal entry (backport #59262)
2026-09-23 12:36:37 +05:30
Khushi Rawat
54c094ed46 Merge pull request #59305 from frappe/mergify/bp/version-16-hotfix/pr-59304
fix: count complete opening depreciation periods accurately (backport #59304)
2026-09-23 12:20:40 +05:30
khushi8112
d4105c3aaf test(assets): cover complete opening depreciation periods
(cherry picked from commit 3691ca5b5b)
2026-09-23 06:38:24 +00:00
khushi8112
900eb1e466 fix(assets): count complete opening depreciation periods accurately
(cherry picked from commit 56f24a6adf)
2026-09-23 06:38:24 +00:00
Sudharsanan Ashok
cfa5c25215 Merge pull request #59297 from frappe/mergify/bp/version-16-hotfix/pr-59231
fix(accounts): keep price list within user permissions (backport #59231)
2026-09-22 23:52:42 +05:30
Sudharsanan Ashok
04be8b1218 Merge pull request #59295 from frappe/mergify/bp/version-16-hotfix/pr-59286
fix(stock): keep mapped cost center in stock entry items (backport #59286)
2026-09-22 23:46:11 +05:30
Sudharsanan11
98fa0d4cb4 fix: resolve conflicts 2026-09-22 21:35:38 +05:30
Sudharsanan Ashok
385343a389 fix(accounts): keep price list within user permissions (#59231)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit e814d13126)
2026-09-22 21:35:38 +05:30
Mihir Kandoi
4186d6c994 fix(stock): deduct rejected qty from the in-transit warehouse (#59299) 2026-09-22 14:24:03 +00:00
Sudharsanan Ashok
06d55a089a Merge pull request #59293 from frappe/mergify/bp/version-16-hotfix/pr-59250
fix(stock): seed bin values when cancelling a stock voucher (backport #59250)
2026-09-22 18:40:42 +05:30
Sudharsanan Ashok
cdbdcc6e33 fix(stock): keep mapped cost center in stock entry items (#59286)
(cherry picked from commit 7f608f31eb)
2026-09-22 12:35:50 +00:00
Sudharsanan Ashok
3ab13e12d4 fix(stock): seed bin values when cancelling a stock voucher (#59250)
* fix(stock): seed bin values when cancelling a stock voucher

cancellation flags every sle of the voucher before update_entries_after
runs, so get_sle_against_current_voucher returns nothing and the seeding
added in #57380 never fires. prev_sle_dict stays empty, update_bin()
writes nothing, and the bin keeps the stock value and valuation rate it
had before the cancellation while its quantity is restored.

seed from the args when the query comes back empty, leaving the existing
anchor in place whenever a live entry shares the posting datetime.

* test(stock): cover bin stock value after cancelling a transfer

a transfer between two warehouses that both hold stock, then cancelled:
both bins must return to their previous quantity, valuation rate and
stock value. fails on develop with 500.0 != 1000.

(cherry picked from commit 62cb38d36b)
2026-09-22 12:23:29 +00:00
khushi8112
0db0cc55d4 fix: apply journal entry reversal backport to version 16 controller 2026-09-22 17:29:06 +05:30
Mihir Kandoi
6af589208b fix(stock): use net purchase values in landed cost vouchers (backport #59274) (#59278)
Co-authored-by: pandiyan <pandiyanpalani37@gmail.com>
2026-09-22 11:50:24 +00:00
ervishnucs
06cc2914ba fix(journal-entry): carry reference fields into reverse journal entry
(cherry picked from commit 9e01992217)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/mapper.py
2026-09-22 11:47:35 +00:00
Khushi Rawat
8e9c0f6e41 Merge pull request #59273 from frappe/mergify/bp/version-16-hotfix/pr-59180
fix: refresh price list exchange rate when currency changes (backport #59180)
2026-09-22 16:59:51 +05:30
Pandiyan P
8eb906366d fix(crm): keep opportunity type default untranslated (#59277)
(cherry picked from commit 79fd06a1ba)
2026-09-22 11:07:03 +00:00
rohitwaghchaure
06a8faa4ed fix: do not zero out backdated stock at a stock reco adjustment entry (#59270)
* fix: do not zero out backdated stock at a stock reco adjustment entry

* fix: keep a stock reco adjustment entry value-only on cancel and refresh

* fix: read stock reco adjustment rows once and value them from the ledger
2026-09-22 16:30:01 +05:30
khushi8112
ff0841e0cb test: cover price list exchange rate currency changes
(cherry picked from commit 221baabf9e)
2026-09-22 10:00:52 +00:00
khushi8112
0fe41ce92e fix: refresh price list exchange rate when currency changes
(cherry picked from commit 8b59d66f94)
2026-09-22 10:00:52 +00:00
Mihir Kandoi
aaaba7cd82 fix: skip serial nos with no stock when reconciling specific serial nos (backport #59266) (#59267) 2026-09-22 09:53:16 +00:00
mergify[bot]
814d7f0c4e feat(selling): group Sales Order Analysis by item (backport #59236) (#59246)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-22 08:35:00 +00:00
mergify[bot]
0f1cefa918 fix(stock): compare transit quantities in stock UOM (backport #59202) (#59244)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-22 08:34:11 +00:00
Sudharsanan Ashok
6d89487518 Merge pull request #59255 from frappe/mergify/bp/version-16-hotfix/pr-59192
fix(stock): apply transit warehouse filter to target warehouse in items table (backport #59192)
2026-09-22 13:39:00 +05:30
Shllokkk
fe8199bece Merge pull request #59259 from frappe/mergify/bp/version-16-hotfix/pr-59240
fix: asset field unfilterable on manual Asset Depreciation Schedule (backport #59240)
2026-09-22 13:16:24 +05:30
Shllokkk
a436707efc fix: remove company link filter on asset field in Asset Depreciation Schedule
(cherry picked from commit a931c7d2f4)
2026-09-22 07:07:59 +00:00
Afsal Syed
ae96dad151 fix(stock): apply transit warehouse filter to target warehouse in items table (#59192)
(cherry picked from commit 893fc75df4)
2026-09-22 12:34:57 +05:30
Sudharsanan Ashok
d4b1be1154 Merge pull request #59194 from frappe/mergify/bp/version-16-hotfix/pr-59191
fix(stock): validate warehouse account belongs to selected company (backport #59191)
2026-09-22 12:04:15 +05:30
Afsal Syed
38bdb4f9c0 fix(stock): validate warehouse account belongs to selected company (#59191)
(cherry picked from commit db6e089109)
2026-09-22 11:42:31 +05:30
Shllokkk
33e86edc91 Merge pull request #59234 from frappe/mergify/bp/version-16-hotfix/pr-59144
fix: use correct exchange rate for multi-currency bank reconciliation payments (backport #59144)
2026-09-21 17:58:53 +05:30
Shllokkk
427a20c508 fix: use correct exchange rate for multi-currency bank reconciliation payments (#59144)
* fix: use correct exchange rate for multi-currency bank reconciliation payments

* test: exchange rate for multi-currency bank reconciliation payment

(cherry picked from commit 04c949a662)
2026-09-21 12:00:43 +00:00
Diptanil Saha
854b0e4734 fix(permissions): grant select on link targets to roles with write access (#59123) 2026-09-21 16:56:53 +05:30
Sudharsanan Ashok
9b8c35f431 Merge pull request #59227 from frappe/mergify/bp/version-16-hotfix/pr-59220
fix(stock): honour pick manually when mapping a pick list (backport #59220)
2026-09-21 15:33:18 +05:30
Sudharsanan11
7ff2a33a5b fix: resolve conflicts 2026-09-21 14:59:27 +05:30
Sudharsanan Ashok
3ee9353240 fix(stock): honour pick manually when mapping a pick list (#59220)
(cherry picked from commit 6e87694bbe)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/mapper.py
#	erpnext/stock/doctype/material_request/mapper.py
2026-09-21 14:59:27 +05:30
Shllokkk
4455ffcae1 Merge pull request #59204 from Shllokkk/fix-gp-returned-invoices-company-filter
perf: filter by company in get_returned_invoice_items
2026-09-21 14:32:02 +05:30
Shllokkk
ff29e14353 Merge branch 'version-16-hotfix' into fix-gp-returned-invoices-company-filter 2026-09-21 13:44:22 +05:30
mergify[bot]
4af1b0db4e perf: skip legacy batch ledger lookups when no legacy entry exists (backport #59110) (#59111)
* perf: skip legacy batch ledger lookups when no legacy entry exists (#59110)

* perf: skip legacy batch ledger lookups when no legacy entry exists

(cherry picked from commit 0130d287f6)

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-09-21 13:37:46 +05:30
Khushi Rawat
75c1a7fd69 Merge pull request #59222 from frappe/mergify/bp/version-16-hotfix/pr-59128
fix: validate coupon pricing rules and validity dates (backport #59128)
2026-09-21 13:23:11 +05:30
MochaMind
d217283229 fix: sync translations from crowdin (version-16-hotfix) (#59210)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-21 09:52:07 +02:00
rohitwaghchaure
8bbb73d2e2 fix: write off stranded stock value only when the warehouse is empty (#59216) 2026-09-21 13:16:38 +05:30
pandiyan
fa6b3380b9 test: cover coupon pricing rule and date validation
(cherry picked from commit 3384c1939b)
2026-09-21 07:17:20 +00:00
pandiyan
2db2244852 fix: validate coupon pricing rules and validity dates
(cherry picked from commit 14275e4c01)
2026-09-21 07:17:19 +00:00
Sudharsanan Ashok
f5eb2d4a0b Merge pull request #59173 from frappe/mergify/bp/version-16-hotfix/pr-59170
fix(stock): respect manual picking when mapping sales orders (backport #59170)
2026-09-21 11:12:05 +05:30
MochaMind
6661c30db4 chore: update POT file (#59200) 2026-09-20 21:43:11 +02:00
Shllokkk
fa998703bf Merge pull request #59203 from Shllokkk/migrate-gp-returned-invoices-to-qb
refactor: migrate raw SQL queries in gross profit report to query builder
2026-09-20 19:53:28 +05:30
Shllokkk
423aa09f59 perf: filter by company in get_returned_invoice_items 2026-09-20 19:38:06 +05:30
Shllokkk
fd369a5a0f refactor: migrate raw SQL queries in gross profit report to query builder 2026-09-20 19:38:02 +05:30
rohitwaghchaure
af334dabcc fix: valuation of serialized items after disabling serial no wise valuation (#59197)
* fix: recalculate outgoing rate on repost after disabling serial no wise valuation

* fix: keep valuation method when serial no wise valuation was already disabled
2026-09-19 22:51:18 +05:30
Sudharsanan Ashok
0fbea6021b Merge pull request #59165 from frappe/mergify/bp/version-16-hotfix/pr-59160
fix(stock): forward company to outward batch lookup (backport #59160)
2026-09-18 18:27:09 +05:30
Khushi Rawat
236464e6cc Merge pull request #59184 from frappe/mergify/bp/version-16-hotfix/pr-59181
fix(accounts): clarify revaluation journal filters (backport #59181)
2026-09-18 17:57:53 +05:30
pandiyan
58847de59a fix(stock): respect manual picking when mapping sales orders 2026-09-18 17:54:10 +05:30
pandiyan
eebd0bc533 fix(accounts): clarify revaluation journal filters
(cherry picked from commit dc4b390a9b)
2026-09-18 12:23:25 +00:00
rohitwaghchaure
c055faabc1 feat(stock): GL-only reposting from Stock and Account Value Comparison report (#59127)
* feat(stock): repost only GL entries from the value comparison report

Adds a "Create GL Reposting Entries" button to the Stock and Account Value
Comparison report, next to the existing "Create Reposting Entries" which is
unchanged. It queues Repost Item Valuation entries with
`repost_only_accounting_ledgers` set, so the General and Payment Ledger are
rebuilt for the selected vouchers while the stock ledgers and valuation rates
are left untouched. This is for the case where stock valuation is already
correct and only the accounting ledger has drifted, which avoids paying for a
full revaluation.

Rows of ledger type "GL Entry" are rejected: they have accounting entries but
no stock ledger entries to rebuild them from, so a GL-only repost would just
wipe their GL. The same restriction is enforced on Repost Item Valuation for
callers outside the report.

Repeated selections are deduplicated, and vouchers that already have a queued
or in-progress GL-only repost are skipped.

* feat(stock): bound GL reposting by a From Date, skip non-stock rows

* fix: batch the pending GL repost lookup and index it on existing sites
2026-09-18 17:02:15 +05:30
Sudharsanan Ashok
62f560a91d Merge pull request #59159 from frappe/mergify/bp/version-16-hotfix/pr-59155
fix(stock): continue putaway allocation after undersized whole-uom rules (backport #59155)
2026-09-18 15:16:23 +05:30
Pandiyan P
5e1cfa4f52 fix(stock): forward company to outward batch lookup (#59160)
(cherry picked from commit 622b7e0caa)
2026-09-18 09:07:13 +00:00
Khushi Rawat
a28767599f Merge pull request #59163 from frappe/mergify/bp/version-16-hotfix/pr-59142
fix: validate shipping rule account company (backport #59142)
2026-09-18 14:33:05 +05:30
pandiyan
f61caa5211 test: cover shipping rule account company validation
(cherry picked from commit 022ee73d78)
2026-09-18 14:18:08 +05:30
pandiyan
dbb8981fa1 fix(stock): continue putaway allocation after undersized whole-uom rules (#59155)
(cherry picked from commit 6cc500e28d)
2026-09-18 14:16:00 +05:30
Khushi Rawat
816880b010 Merge pull request #59157 from frappe/mergify/bp/version-16-hotfix/pr-59081
fix(accounts): validate bank reconciliation date ranges (backport #59081)
2026-09-18 14:13:24 +05:30
pandiyan
590fff6a45 fix: validate shipping rule account company
(cherry picked from commit 340feb94a1)
2026-09-18 08:37:20 +00:00
pandiyan
0634cf31c7 test: cover bank reconciliation date validation
(cherry picked from commit 62d0e2c82e)

# Conflicts:
#	erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
2026-09-18 13:54:27 +05:30
pandiyan
a878951c17 fix: validate bank reconciliation date ranges
(cherry picked from commit 970f32a342)

# Conflicts:
#	erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
2026-09-18 13:51:57 +05:30
Sudharsanan Ashok
56db17919b Merge pull request #59126 from frappe/mergify/bp/version-16-hotfix/pr-59116
fix: retain bom components without warehouse stock (backport #59116)
2026-09-18 11:55:49 +05:30
Sudharsanan Ashok
11cbcbe02a Merge pull request #59119 from frappe/mergify/bp/version-16-hotfix/pr-59104
fix(stock): allow zero completed quantity and handle process loss in job cards (backport #59104)
2026-09-18 11:55:10 +05:30
Sudharsanan Ashok
5d4271b1c7 Merge pull request #59145 from frappe/mergify/bp/version-16-hotfix/pr-59143
fix(manufacturing): include the mrp bucket that ends on to_date (backport #59143)
2026-09-18 11:40:01 +05:30
Sudharsanan Ashok
cbd932e20d Merge pull request #59093 from frappe/mergify/bp/version-16-hotfix/pr-59007
fix(manufacturing): apply mrp manufacturing buffer as a duration (backport #59007)
2026-09-18 11:12:56 +05:30
Sudharsanan Ashok
4ad08db1d6 Merge pull request #59090 from frappe/mergify/bp/version-16-hotfix/pr-58923
fix(stock): filter item group defaults by company and non-group records (backport #58923)
2026-09-18 11:12:02 +05:30
Sudharsanan11
131343de28 fix(manufacturing): include the mrp bucket that ends on to_date (#59143)
(cherry picked from commit 5d5eeb5e02)
2026-09-18 11:00:06 +05:30
rohitwaghchaure
16b1821f35 perf: reduce memory consumption during reposting (backport #59117) (#59149)
* perf: reduce memory consumption during reposting

The reposting queue introduced in #52152 keeps the complete future stock
ledger of every transitively dependent item-warehouse in a single deque.
Each `select *` row is ~3.5KB, so a repost spanning a million entries
needs several GB and gets OOM killed on smaller workers.

- Queue only the identity and sort keys of an entry, and fetch the full
  row in batches of 500 just before it is processed
- Move `for update` off the bulk prefetch onto the batch, so millions of
  rows are no longer locked for the whole duration of the repost
- Drop the process local document cache and message log at every
  checkpoint, neither is evicted within a long running job

* fix: keep recent repost messages and log skipped entries

Trimming `frappe.local.message_log` to empty at every checkpoint discarded
warnings that the Repost Item Valuation failure handler reads back when
building the error log. Keep the most recent messages instead, which bounds
the growth just as well.

Also log when a queued entry is no longer active by the time its batch is
loaded, rather than skipping it silently.

* fix: skip reposts already covered by a Manufacture/Repack dependant repost

While reposting a raw material, the finished goods produced from it are
reposted as dependants, from the posting datetime of the manufacture entry
through to the end of their ledger. A repost queued separately for the same
finished good and warehouse at a later datetime therefore has nothing left to
do, but it was still picked up and walked the same entries again.

Track the item-warehouse combinations pulled in as dependants of a Manufacture
or Repack entry, and mark the redundant queued reposts as Skipped once the
dependants have been reposted. This runs per item being reposted, so the
finished good's queued repost is released without waiting for the whole raw
material repost to finish.

Only 'Item and Warehouse' reposts are skipped. A 'Transaction' repost spans
several item-warehouse combinations, so covering one says nothing about the
rest. Reposts starting before the manufacture entry still have work to do and
are left queued.

* fix: don't lock the whole repost queue prefetch

* test: repost covers every entry once across batches
2026-09-18 09:03:48 +05:30
mergify[bot]
d2042bc1bb feat: mark update_serial_batch_entries patch as never skippable (backport #59147) (#59148)
feat: mark update_serial_batch_entries patch as never skippable (#59147)

(cherry picked from commit aebb160e76)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-09-17 17:10:48 +00:00
Sudharsanan Ashok
652fc96e85 Merge pull request #59088 from frappe/mergify/bp/version-16-hotfix/pr-58998
fix(stock): use stored posting_datetime when reposting stock ledger entries (backport #58998)
2026-09-17 18:44:36 +05:30
Sudharsanan11
6fab4e0203 fix: resolve conflicts 2026-09-17 18:28:20 +05:30
Sudharsanan Ashok
39d6f2a236 fix(stock): use stored posting_datetime when reposting stock ledger entries (#58998)
* fix(stock): use stored posting_datetime for repost boundary

get_stock_ledger_entries re-derived posting_datetime from posting_date and
posting_time on every call, discarding the stored value its callers pass in.
when a row's stored posting_datetime differs from that pair, the replay window
is built from the wrong instant: the row falls outside the range filter and is
never recomputed, while get_previous_sle still selects it as the opening
balance and reuses its stale qty_after_transaction. every later entry inherits
the error, leaving bin qty adrift from the sum of its ledger.

derive the boundary only when the caller has not supplied one.

* fix(stock): match current voucher sle on stored posting_datetime

get_sle_against_current_voucher selected rows with an equality check against a
posting_datetime re-derived from posting_date and posting_time. a row whose
stored posting_datetime differs from that pair matches nothing, so reposting
the voucher silently processes zero entries and the row can never be corrected
through its own voucher.

read the timestamp from the stored row when the sle is known, and derive it
only as a fallback.

* test(stock): cover repost with diverged posting_datetime

add a repack scenario whose incoming entry stores a posting_datetime one
microsecond before its own posting_time. asserts the voucher lookup still
finds that entry, and that reposting replays it instead of reusing its stale
qty_after_transaction, which otherwise left bin qty at 115 against 615 of
recorded movements.

(cherry picked from commit 6cee9c330c)

# Conflicts:
#	erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
2026-09-17 18:28:20 +05:30
Afsal Syed
4f984aeecf fix(stock): allow zero completed quantity and handle process loss in job cards (#59104)
(cherry picked from commit 1d8ce1ee8c)
2026-09-17 13:47:17 +05:30
pandiyan
18ee3b9cc0 fix: retain bom components without warehouse stock (#59116)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 60913b722a)
2026-09-17 11:16:48 +05:30
Sudharsanan Ashok
3eca07169e Merge pull request #59098 from frappe/mergify/bp/version-16-hotfix/pr-59097
fix: avoid pricing rule help template errors (backport #59097)
2026-09-17 11:09:26 +05:30
Sudharsanan Ashok
3acf4e7606 Merge pull request #59096 from frappe/mergify/bp/version-16-hotfix/pr-58899
fix: clear stock value for zero-quantity balances (backport #58899)
2026-09-17 11:08:34 +05:30
pandiyan
007a1f5528 fix: clear stock value for zero-quantity balances (#58899)
* fix: clear stock value for zero-quantity balances

* test(stock): cover zero-quantity residual stock values

(cherry picked from commit b439bdda68)
2026-09-17 10:52:57 +05:30
Shllokkk
c42df7ef3e Merge pull request #59108 from frappe/mergify/bp/version-16-hotfix/pr-59078
fix: render letter head jinja in statement of accounts pdf (backport #59078)
2026-09-16 16:44:24 +05:30
Shllokkk
31111a4a30 fix: render letter head jinja in statement of accounts pdf
(cherry picked from commit da377683d6)
2026-09-16 10:10:24 +00:00
mergify[bot]
877747c133 fix: update modified for last purchase rate (backport #59044) (#59106)
fix: update modified for last purchase rate (#59044)

(cherry picked from commit f6b907ef9d)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-09-16 15:37:19 +05:30
rohitwaghchaure
86fe0c1f4b perf(stock): chunk the serial and batch entry backfill patch (#59076)
* perf(stock): chunk the serial and batch entry backfill patch

* fix(stock): support postgres in the serial and batch entry backfill patch
2026-09-16 15:23:08 +05:30
rohitwaghchaure
262fdf3e69 fix: negative stock value for moving average item with mixed batchwise valuation (#59099)
* fix: negative stock value for moving average item with mixed batchwise valuation

* chore: remove redundant docstring

* test: restore frappe flags in a finally block
2026-09-16 14:49:01 +05:30
Pandiyan P
e3ef3be490 fix: avoid pricing rule help template errors (#59097)
(cherry picked from commit 2273448f1b)
2026-09-16 07:40:46 +00:00
rohitwaghchaure
f09ce0583f feat: use serial no wise valuation switch on item (#59082)
* perf: speed up legacy serial no valuation lookup

* feat: use serial no wise valuation switch on item

* fix: clear stale valuation method cache on item update

* fix: force moving average only when stock transactions exist

* fix: correct serial no wise valuation field description

* fix: add type hints to whitelisted get_incoming_rate args
2026-09-16 12:24:18 +05:30
pandiyan
fa732ca3f4 fix(manufacturing): apply mrp manufacturing buffer as a duration (#59007)
(cherry picked from commit 2ad4a8c4a4)

Include the material-type helper required by the duration calculation on version 16.
2026-09-16 12:05:39 +05:30
pandiyan
741ae5d33c fix(stock): filter item group defaults by company and non-group records (#58923)
(cherry picked from commit 00ee694444)
2026-09-16 12:05:13 +05:30
Sudharsanan Ashok
378c756d52 Merge pull request #59016 from frappe/mergify/bp/version-16-hotfix/pr-58994
fix(stock): calculate batch bundle valuation per unit (backport #58994)
2026-09-16 11:03:43 +05:30
mergify[bot]
3c9d9d62aa fix(asset): skip missing checkbox columns in asset type patch (backport #58416) (#58417)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-09-16 01:39:55 +02:00
Pandiyan P
110192d9de fix(stock): calculate batch bundle valuation per unit (#58994)
(cherry picked from commit 825d24f406)
2026-09-11 12:51:30 +00:00
733 changed files with 99912 additions and 72522 deletions

View File

@@ -23,6 +23,19 @@ jobs:
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.1
js-unit-tests:
name: js unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Run JS unit tests
run: yarn test:js
semgrep:
name: semgrep
runs-on: ubuntu-latest

View File

@@ -55,6 +55,9 @@ class ERPNextAddress(Address):
@frappe.whitelist()
def get_shipping_address(company, address=None):
# `select`, not `read`: Delivery, Maintenance, Purchase and Stock Manager fill in forms that ask
# for this while holding no Company `read` row. doc= so User Permissions apply to the named company.
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],

View File

@@ -24,7 +24,7 @@ def get(
heatmap_year=None,
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name)
chart = frappe.get_doc("Dashboard Chart", chart_name, check_permission="read")
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
@@ -53,6 +53,9 @@ def get(
)
)
# authorise the account itself, as get_balance_on() does; doc= brings User Permissions with it
frappe.has_permission("Account", doc=account, throw=True)
if not to_date:
to_date = nowdate()
if not from_date:

View File

@@ -7,6 +7,7 @@
"description": "Heads (or groups) against which Accounting Entries are made and balances are maintained.",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/chart-of-accounts",
"engine": "InnoDB",
"field_order": [
"properties",
@@ -122,6 +123,7 @@
"description": "Setting Account Type helps in selecting this Account in transactions.",
"fieldname": "account_type",
"fieldtype": "Select",
"in_preview": 1,
"in_standard_filter": 1,
"label": "Account Type",
"oldfieldname": "account_type",
@@ -199,11 +201,11 @@
"options": "Account Category"
}
],
"icon": "fa fa-money",
"icon": "vault",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:14:42.202065",
"modified": "2026-09-25 15:33:56.058228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -264,6 +266,46 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -503,12 +503,19 @@ class Account(NestedSet):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = {}
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, page_len, start),
as_list=1,
return frappe.get_list(
"Account",
filters=[
["is_group", "=", 1],
["docstatus", "!=", 2],
["company", "=", filters["company"]],
[searchfield, "like", f"%{txt}%"],
],
fields=["name"],
order_by="name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)

View File

@@ -52,6 +52,42 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the account number renders as a badge (see
// onrender) instead of being glued into the name
return frappe.utils.escape_html(node.data.account_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.account_number) {
flags.push(frappe.ui.badge({ label: node.data.account_number }));
}
const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value();
const company_currency = company && erpnext.get_currency(company);
if (
node.data.account_currency &&
company_currency &&
node.data.account_currency !== company_currency
) {
flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue" }));
}
if (node.data.freeze_account === "Yes") {
flags.push(
frappe.ui.badge({
label: __("Frozen"),
icon: "lock",
title: __("Frozen - entries restricted"),
theme: "orange",
})
);
}
erpnext.utils.render_tree_node_flags(node, flags);
},
on_node_render: function (node, deep) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
@@ -232,7 +268,7 @@ frappe.treeview_settings["Account"] = {
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
if (treeview.can_create) {
treeview.page.set_primary_action(
__("New"),
{ label: __("Add Account"), short_label: __("Add") },
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
@@ -243,13 +279,14 @@ frappe.treeview_settings["Account"] = {
treeview.new_node();
}
},
"add"
"plus"
);
}
},
toolbar: [
{
label: __("Add Child"),
icon: "plus",
condition: function (node) {
return (
frappe.boot.user.can_create.indexOf("Account") !== -1 &&
@@ -272,6 +309,7 @@ frappe.treeview_settings["Account"] = {
return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1;
},
label: __("View Ledger"),
icon: "book-open",
click: function (node, btn) {
frappe.route_options = {
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
@@ -286,6 +324,106 @@ frappe.treeview_settings["Account"] = {
},
btnClass: "hidden-xs",
},
{
// same label and mechanism as the Account form's Actions button:
// NOT frappe's generic rename (Allow Rename stays off) — this is
// ERPNext's controlled update that rebuilds the derived
// "number - name - abbr" document name
label: __("Update Account Name / Number"),
icon: "text-cursor-input",
condition: function (node) {
return !node.is_root && frappe.model.can_write("Account");
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __("Update Account Number / Name"),
fields: [
{
fieldtype: "Data",
fieldname: "account_name",
label: __("Account Name"),
reqd: 1,
default: node.data.account_name,
},
{
fieldtype: "Data",
fieldname: "account_number",
label: __("Account Number"),
default: node.data.account_number,
},
],
primary_action_label: __("Update"),
primary_action(values) {
dialog.hide();
frappe.dom.freeze(__("Updating {0}", [node.label]));
frappe.call({
method: "erpnext.accounts.doctype.account.account.update_account_number",
args: {
name: node.label,
account_name: values.account_name,
account_number: values.account_number,
},
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees["Account"];
node.parent_node && treeview.tree.load_children(node.parent_node);
},
always: function () {
frappe.dom.unfreeze();
},
});
},
});
dialog.show();
},
},
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Account");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Account")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -102,6 +102,8 @@ def identify_is_group(child):
def get_chart(chart_template, existing_company=None):
chart = {}
if existing_company:
frappe.has_permission("Company", doc=existing_company, throw=True)
return get_account_tree_from_existing_company(existing_company)
elif chart_template == "Standard":

View File

@@ -40,6 +40,7 @@
}
],
"grid_page_length": 50,
"icon": "folder-tree",
"index_web_pages_for_search": 1,
"links": [
{

View File

@@ -147,7 +147,7 @@
"precision": "9"
}
],
"icon": "fa fa-list",
"icon": "scale",
"in_create": 1,
"links": [],
"modified": "2025-08-22 19:13:50.400404",

View File

@@ -3,6 +3,7 @@
"autoname": "field:label",
"creation": "2019-05-04 18:13:37.002352",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimensions",
"engine": "InnoDB",
"field_order": [
"document_type",
@@ -49,6 +50,7 @@
"options": "Accounting Dimension Detail"
}
],
"icon": "layers",
"links": [],
"modified": "2024-03-27 13:05:56.890002",
"modified_by": "Administrator",

View File

@@ -235,8 +235,11 @@ def delete_accounting_dimension(doc):
frappe.clear_cache(doctype=doctype)
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def disable_dimension(doc):
# toggle_disabling rewrites a Custom Field site-wide, so demand the write that configures dimensions
frappe.has_permission("Accounting Dimension", "write", throw=True)
if frappe.in_test:
toggle_disabling(doc=doc)
else:

View File

@@ -6,7 +6,7 @@ frappe.ui.form.on("Accounting Dimension Filter", {
let help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
</p>
</td></tr>

View File

@@ -3,6 +3,7 @@
"autoname": "format:{accounting_dimension}-{#####}",
"creation": "2020-11-08 18:28:11.906146",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimension-filter",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -99,6 +100,7 @@
"label": "Fieldname"
}
],
"icon": "funnel",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-08 14:13:22.203011",

View File

@@ -3,6 +3,7 @@
"autoname": "field:period_name",
"creation": "2018-04-13 18:50:14.672323",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-period",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -77,6 +78,7 @@
"options": "Role"
}
],
"icon": "calendar-range",
"links": [],
"modified": "2026-03-09 17:15:33.577217",
"modified_by": "Administrator",

View File

@@ -3,6 +3,7 @@
"creation": "2013-06-24 15:49:57",
"doctype": "DocType",
"document_type": "Other",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounts-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -793,7 +794,7 @@
}
],
"grid_page_length": 50,
"icon": "icon-cog",
"icon": "settings",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,

View File

@@ -4,6 +4,7 @@
"creation": "2024-10-16 16:57:12.085072",
"doctype": "DocType",
"engine": "InnoDB",
"icon": "book-open",
"field_order": [
"company",
"voucher_type",

View File

@@ -6,6 +6,7 @@
"creation": "2018-04-07 16:59:59.496668",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -100,8 +101,9 @@
"read_only": 1
}
],
"icon": "landmark",
"links": [],
"modified": "2024-03-27 13:06:36.896195",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
@@ -118,6 +120,14 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -5,6 +5,7 @@
"creation": "2017-05-29 21:35:13.136357",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-account",
"engine": "InnoDB",
"field_order": [
"account_name",
@@ -227,6 +228,7 @@
}
],
"grid_page_length": 50,
"icon": "credit-card",
"links": [
{
"group": "Transactions",
@@ -269,7 +271,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2026-04-11 19:46:27.609994",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -299,6 +301,22 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -44,6 +44,7 @@
}
],
"grid_page_length": 50,
"icon": "wallet",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-16 22:17:48.007982",

View File

@@ -18,6 +18,7 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.221876",
"modified_by": "Administrator",

View File

@@ -18,6 +18,7 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.347035",
"modified_by": "Administrator",

View File

@@ -87,7 +87,7 @@
}
],
"hide_toolbar": 1,
"icon": "fa fa-check",
"icon": "badge-check",
"idx": 1,
"issingle": 1,
"links": [],

View File

@@ -5,6 +5,7 @@
"creation": "2016-12-17 10:43:35.731631",
"doctype": "DocType",
"document_type": "Document",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-guarantee",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -216,6 +217,7 @@
}
],
"grid_page_length": 50,
"icon": "shield-check",
"is_submittable": 1,
"links": [],
"modified": "2026-05-25 18:12:10.768835",

View File

@@ -60,6 +60,9 @@ def get_voucher_details(bank_guarantee_type: str, reference_name: str):
if not isinstance(reference_name, str):
raise TypeError("reference_name must be a string")
# the form is the boundary, not the referenced order: an order guard would break one of the two roles
frappe.has_permission("Bank Guarantee", throw=True)
fields_to_fetch = ["grand_total"]
if bank_guarantee_type == "Receiving":

View File

@@ -68,6 +68,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frappe.msgprint(__("Please select Bank Account"));
return;
}
frm.events.validate_dates(frm);
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
args: {
@@ -82,7 +83,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
});
frm.add_custom_button(__("Get Unreconciled Entries"), function () {
frm.trigger("make_reconciliation_tool");
return frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type(__("Get Unreconciled Entries"), null, "primary");
@@ -106,7 +107,24 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.trigger("get_account_opening_balance");
},
validate_dates(frm) {
const from_date = frm.doc.filter_by_reference_date
? frm.doc.from_reference_date
: frm.doc.bank_statement_from_date;
const to_date = frm.doc.filter_by_reference_date
? frm.doc.to_reference_date
: frm.doc.bank_statement_to_date;
if (from_date && to_date && from_date > to_date) {
frappe.throw(
frm.doc.filter_by_reference_date
? __("From Reference Date cannot be greater than To Reference Date")
: __("From Date cannot be greater than To Date")
);
}
},
make_reconciliation_tool(frm) {
frm.events.validate_dates(frm);
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
frm.trigger("get_cleared_balance").then(() => {

View File

@@ -116,6 +116,7 @@
}
],
"hide_toolbar": 1,
"icon": "arrow-left-right",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -10,7 +10,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import cint, create_batch, flt
from frappe.utils import cint, create_batch, flt, getdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -55,6 +55,8 @@ def get_bank_transactions(
all_transactions: bool = False,
):
# returns bank transactions for a bank account
validate_date_range(from_date, to_date)
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
@@ -794,6 +796,7 @@ def create_bulk_payment_entry_and_reconcile(
"deposit",
"withdrawal",
"bank_account",
"company",
"currency",
"unallocated_amount",
"date",
@@ -828,11 +831,7 @@ def create_bulk_payment_entry_and_reconcile(
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": bank_transaction.unallocated_amount,
"base_paid_amount": bank_transaction.unallocated_amount,
"received_amount": bank_transaction.unallocated_amount,
"base_received_amount": bank_transaction.unallocated_amount,
"target_exchange_rate": 1,
"source_exchange_rate": 1,
"reference_date": bank_transaction.date,
"posting_date": bank_transaction.date,
"reference_no": (bank_transaction.reference_number or bank_transaction.description or "")[
@@ -841,6 +840,8 @@ def create_bulk_payment_entry_and_reconcile(
}
)
set_multi_currency_amounts(payment_entry_doc)
payment_entry_doc.insert()
payment_entry_doc.submit()
@@ -879,6 +880,7 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
"doctype": "Payment Entry",
}
)
set_multi_currency_amounts(payment_entry)
payment_entry.insert()
payment_entry.submit()
transaction = reconcile_vouchers(
@@ -901,6 +903,33 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
}
def set_multi_currency_amounts(pe):
"""Set real exchange rates when the bank and party accounts differ in currency."""
company_currency = frappe.get_cached_value("Company", pe.company, "default_currency")
pe.paid_from_account_currency = frappe.get_cached_value("Account", pe.paid_from, "account_currency")
pe.paid_to_account_currency = frappe.get_cached_value("Account", pe.paid_to, "account_currency")
pe.source_exchange_rate = (
1.0
if pe.paid_from_account_currency == company_currency
else get_exchange_rate(pe.paid_from_account_currency, company_currency, pe.posting_date)
)
pe.target_exchange_rate = (
1.0
if pe.paid_to_account_currency == company_currency
else get_exchange_rate(pe.paid_to_account_currency, company_currency, pe.posting_date)
)
# derive the party-side amount from the authoritative bank-side amount; Payment Entry books any
# rounding residual to Exchange Gain/Loss during validation (set_exchange_gain_loss)
if pe.payment_type == "Receive" and pe.source_exchange_rate:
base_amount = flt(pe.received_amount) * pe.target_exchange_rate
pe.paid_amount = flt(base_amount / pe.source_exchange_rate, pe.precision("paid_amount"))
elif pe.payment_type == "Pay" and pe.target_exchange_rate:
base_amount = flt(pe.paid_amount) * pe.source_exchange_rate
pe.received_amount = flt(base_amount / pe.target_exchange_rate, pe.precision("received_amount"))
@frappe.whitelist(methods=["GET"])
def search_for_transfer_transaction(transaction_id: str | int):
"""
@@ -969,6 +998,7 @@ def auto_reconcile_vouchers(
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
):
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10:
@@ -1087,6 +1117,7 @@ def get_linked_payments(
to_reference_date: str | date | None = None,
):
# get all matching payments for a bank transaction
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
@@ -1106,6 +1137,23 @@ def get_linked_payments(
return subtract_allocations(gl_account, matching)
def validate_date_range(
from_date,
to_date,
filter_by_reference_date=False,
from_reference_date=None,
to_reference_date=None,
):
if cint(filter_by_reference_date):
from_date, to_date = from_reference_date, to_reference_date
message = _("From Reference Date cannot be greater than To Reference Date")
else:
message = _("From Date cannot be greater than To Date")
if from_date and to_date and getdate(from_date) > getdate(to_date):
frappe.throw(message)
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []

View File

@@ -2,12 +2,16 @@
# See license.txt
from unittest.mock import patch
import frappe
from frappe import qb
from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
auto_reconcile_vouchers,
create_bulk_payment_entry_and_reconcile,
create_payment_entry_and_reconcile,
get_auto_reconcile_message,
get_bank_transactions,
get_linked_payments,
@@ -16,6 +20,8 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
RATE_METHOD = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_exchange_rate"
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
@@ -131,6 +137,33 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
self.assertEqual(names, [])
def test_rejects_reversed_date_ranges(self):
from_date, to_date = today(), add_days(today(), -1)
with self.assertRaisesRegex(frappe.ValidationError, "From Date cannot be greater than To Date"):
get_bank_transactions(self.bank_account, from_date, to_date)
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
auto_reconcile_vouchers(
self.bank_account,
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
transaction = self.make_bank_transaction(date=today())
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
get_linked_payments(
transaction.name,
["payment_entry"],
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
def test_deposit_matches_amount_received_in_bank_account(self):
# money leaves another bank account and lands here minus a charge, so the two sides differ
payment = frappe.get_doc(
@@ -199,3 +232,117 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
self.assertIn("1 Transaction Partially Reconciled", singular)
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
self.assertIn("2 Transactions Partially Reconciled", plural)
def test_multi_currency_pay_converts_and_balances(self):
# withdrawal from an INR bank paying a USD supplier; rate 3.0 makes 100/3 non-exact
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(withdrawal=100),
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
party_account=self.creditors_usd,
paid_from=self.bank,
paid_to=self.creditors_usd,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1) # submits despite the rounding residual
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (1.0, 3.0))
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
# Payment Entry auto-books the rounding residual to Exchange Gain/Loss
self.assertTrue(pe.deductions[0].is_exchange_gain_loss)
self.assertEqual(pe.deductions[0].amount, 0.01) # 100 - 33.33 * 3
def test_multi_currency_receive_converts_and_balances(self):
# deposit into an INR bank from a USD customer; the party side must convert
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(deposit=100),
payment_type="Receive",
party_type="Customer",
party=self.customer,
party_account=self.debtors_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1)
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (3.0, 1.0))
self.assertEqual((pe.received_amount, pe.paid_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
def test_multi_currency_bulk_pay_converts_and_balances(self):
# the bulk path builds the Payment Entry itself, so it must convert too
self.enable_multi_currency_setup()
txn = self.make_multi_currency_txn(withdrawal=100)
with patch(RATE_METHOD, return_value=3.0):
result = create_bulk_payment_entry_and_reconcile(
[txn.name], "Supplier", self.supplier, self.creditors_usd
)
pe = frappe.get_doc("Payment Entry", result[0]["payment_entry"].name)
self.assertEqual(pe.docstatus, 1)
self.assertEqual(pe.target_exchange_rate, 3.0)
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33))
self.assertEqual(pe.difference_amount, 0)
def enable_multi_currency_setup(self):
# USD party/accounts + a company gain/loss account to absorb rounding residuals
self.company_abbr = "_TC"
self.create_supplier(supplier_name="_Test Supplier USD", currency="USD")
self.create_customer(customer_name="_Test Customer USD", currency="USD")
self.create_usd_payable_account()
self.create_usd_receivable_account()
self.set_party_account("Supplier", self.supplier, self.creditors_usd)
if not frappe.db.get_value("Company", self.company, "exchange_gain_loss_account"):
frappe.db.set_value(
"Company", self.company, "exchange_gain_loss_account", "Exchange Gain/Loss - _TC"
)
def set_party_account(self, party_type, party, account):
doc = frappe.get_doc(party_type, party)
if not any(row.company == self.company for row in doc.accounts):
doc.append("accounts", {"company": self.company, "account": account})
doc.save()
def make_multi_currency_txn(self, withdrawal=0, deposit=0):
return (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": today(),
"withdrawal": withdrawal,
"deposit": deposit,
"bank_account": self.bank_account,
"currency": "INR",
"reference_number": "TEST-FX-REF",
}
)
.save()
.submit()
)
def reconcile_new_payment(
self, txn, *, payment_type, party_type, party, party_account, paid_from, paid_to, rate
):
# mimics the /banking frontend, which sends a hardcoded 1:1 rate
payment_entry_doc = {
"payment_type": payment_type,
"company": self.company,
"party_type": party_type,
"party": party,
"party_account": party_account,
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": txn.unallocated_amount,
"received_amount": txn.unallocated_amount,
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"posting_date": today(),
"reference_no": f"TEST-FX-{payment_type}",
"reference_date": today(),
}
with patch(RATE_METHOD, return_value=rate):
result = create_payment_entry_and_reconcile(txn.name, payment_entry_doc)
return frappe.get_doc("Payment Entry", result["payment_entry"].name)

View File

@@ -224,6 +224,7 @@
}
],
"hide_toolbar": 1,
"icon": "file-down",
"links": [],
"modified": "2026-05-30 20:51:10.353723",
"modified_by": "Administrator",

View File

@@ -436,6 +436,9 @@ def get_import_logs(docname: str):
@frappe.whitelist()
def upload_bank_statement(**args):
# The only caller routes into a new Bank Statement Import form, so `create` is the right to require.
frappe.has_permission("Bank Statement Import", "create", throw=True)
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")

View File

@@ -188,6 +188,7 @@
}
],
"grid_page_length": 50,
"icon": "file-clock",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-07-09 17:55:25.615942",

View File

@@ -4,6 +4,7 @@
"autoname": "naming_series:",
"creation": "2018-10-22 18:19:02.784533",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-transaction",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -277,6 +278,7 @@
}
],
"grid_page_length": 50,
"icon": "arrow-left-right",
"is_submittable": 1,
"links": [],
"modified": "2026-04-11 20:41:15.124085",

View File

@@ -11,6 +11,10 @@ from frappe.utils.dateutils import parse_date
@frappe.whitelist()
def upload_bank_statement():
# Parsing a statement is the first step of creating Bank Transactions from it. Both functions here
# are reached only over HTTP, so there is no in-process caller to break.
frappe.has_permission("Bank Transaction", "create", throw=True)
if getattr(frappe, "uploaded_file", None):
with open(frappe.uploaded_file, "rb") as upfile:
fcontent = upfile.read()
@@ -34,8 +38,13 @@ def upload_bank_statement():
return {"columns": columns, "data": data}
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_bank_entries(columns, data, bank_account):
# insert()/submit() enforce this per document, but only after the loop has read each Bank Account
# and written an Error Log per rejected row. Check once up front.
frappe.has_permission("Bank Transaction", "create", throw=True)
frappe.has_permission("Bank Account", doc=bank_account, throw=True)
header_map = get_header_mapping(columns, bank_account)
success = 0

View File

@@ -140,6 +140,7 @@
}
],
"grid_page_length": 50,
"icon": "zap",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-11 21:36:26.754667",

View File

@@ -167,6 +167,7 @@
}
],
"hide_toolbar": 1,
"icon": "split",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -68,6 +68,7 @@
"label": "Generated"
}
],
"icon": "git-branch",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:06:39.766063",

View File

@@ -4,6 +4,7 @@
"autoname": "naming_series:",
"creation": "2016-05-16 11:42:29.632528",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/budgeting",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -330,6 +331,7 @@
}
],
"grid_page_length": 50,
"icon": "piggy-bank",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -122,6 +122,7 @@
"read_only": 1
}
],
"icon": "lock",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:06:44.260440",

View File

@@ -16,6 +16,8 @@ frappe.ui.form.on("Chart of Accounts Importer", {
() => generate_tree_preview(frm),
() => create_import_button(frm),
() => frm.set_df_property("chart_preview", "hidden", 0),
// the preview is the point of this page — open it right away
() => frm.fields_dict.chart_preview.collapse(false),
]);
}
@@ -128,7 +130,6 @@ var create_import_button = function (frm) {
freeze_message: __("Creating Accounts..."),
callback: function (r) {
if (!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__("Import Successful"), "blue");
create_reset_button(frm);
}
@@ -142,42 +143,95 @@ var create_reset_button = function (frm) {
frm.page
.set_primary_action(__("Reset"), function () {
frm.page.clear_primary_action();
delete frm.page["show_import_button"];
frm.reload_doc();
})
.addClass("btn btn-primary");
};
var validate_coa = function (frm) {
if (frm.doc.import_file) {
let parent = __("All Accounts");
return frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
for_validate: 1,
},
callback: function (r) {
if (r.message["show_import_button"]) {
frm.page["show_import_button"] = Boolean(r.message["show_import_button"]);
}
},
});
}
};
var generate_tree_preview = function (frm) {
let parent = __("All Accounts");
$(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
const wrapper = $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
// search + expand/collapse-all lean on frappe.ui.Tree helpers added with
// row mode; when running against an older frappe that predates them, skip
// this toolbar so the preview still renders (just without the extras)
const has_row_helpers =
typeof frappe.ui.Tree.prototype.get_expansion_state === "function" &&
typeof frappe.ui.Tree.prototype.filter_nodes === "function";
let tree;
let deep_loaded = false;
let search_text = "";
let update_buttons = () => {};
if (has_row_helpers) {
// same toolbar anatomy as the tree view: search on the left,
// expand/collapse-all on the right (three-state: fully collapsed ->
// Expand All, fully expanded -> Collapse All, partially expanded -> both)
const $toolbar = $('<div class="flex items-center gap-2 mb-2"></div>').appendTo(wrapper);
const search_control = frappe.ui.form.make_control({
df: { fieldtype: "Data", fieldname: "preview_search", placeholder: __("Search") },
parent: $toolbar,
only_input: true,
});
search_control.refresh();
$(search_control.wrapper).addClass("m-0").css("width", "220px");
search_control.$input.addClass("input-xs");
search_control.$input.on(
"input",
frappe.utils.debounce(() => {
search_text = search_control.$input.val();
const run = () => {
// a newer keystroke superseded this one while the deep load ran
if (search_text !== search_control.$input.val()) return;
tree.filter_nodes(search_text);
};
if (!search_text || deep_loaded) {
run();
return;
}
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
run();
});
}, 300)
);
const $actions = $('<div class="ms-auto flex items-center gap-1"></div>').appendTo($toolbar);
update_buttons = () => {
const state = tree.get_expansion_state();
$expand_all.prop("disabled", !(state === "collapsed" || state === "partial"));
$collapse_all.prop("disabled", !(state === "expanded" || state === "partial"));
};
// tooltip on a wrapper: a disabled es-button has pointer-events:none,
// so hover falls through to the wrapper and the tooltip still shows
const make_action = (icon, label, onclick) => {
const $btn = $(
frappe.ui.button({ icon, disabled: true, onclick, attrs: { "aria-label": label } })
);
const $wrapper = $('<span class="inline-flex"></span>').append($btn).appendTo($actions);
frappe.ui.tooltip($wrapper, { text: label });
return $btn;
};
var $expand_all = make_action("chevrons-up-down", __("Expand All"), () => {
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
});
});
var $collapse_all = make_action("chevrons-down-up", __("Collapse All"), () => {
tree.load_children(tree.root_node, false);
});
}
// generate tree structure based on the csv data
return new frappe.ui.Tree({
parent: $(frm.fields_dict["chart_tree"].wrapper),
tree = new frappe.ui.Tree({
parent: wrapper,
label: parent,
expandable: true,
// read-only preview: row-mode visuals without actions or hover cards
// (ignored by an older frappe, which renders the legacy tree)
row_style: true,
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
@@ -185,8 +239,9 @@ var generate_tree_preview = function (frm) {
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
},
onclick: function (node) {
parent = node.value;
},
on_node_render: () => update_buttons(),
// expanded flips right after this callback — check on the next tick
on_click: () => setTimeout(update_buttons, 0),
});
return tree;
};

View File

@@ -5,6 +5,7 @@
"description": "Import Chart of Accounts from a csv file",
"doctype": "DocType",
"document_type": "Other",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/chart-of-accounts-importer",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -47,6 +48,7 @@
}
],
"hide_toolbar": 1,
"icon": "file-input",
"in_create": 1,
"issingle": 1,
"links": [],

View File

@@ -8,6 +8,7 @@ from functools import reduce
import frappe
from frappe import _
from frappe.core.doctype.file.utils import find_file_by_url
from frappe.desk.form.linked_with import get_linked_fields
from frappe.model.document import Document
from frappe.utils import cint, cstr
@@ -58,6 +59,8 @@ def validate_columns(data):
@frappe.whitelist()
def validate_company(company):
frappe.has_permission("Chart of Accounts Importer", throw=True)
parent_company, allow_account_creation_against_child_company = frappe.get_cached_value(
"Company", company, ["parent_company", "allow_account_creation_against_child_company"]
)
@@ -110,7 +113,10 @@ def import_coa(file_name, company):
def get_file(file_name):
file_doc = frappe.get_doc("File", {"file_url": file_name})
file_doc = find_file_by_url(file_name)
if not file_doc:
raise frappe.PermissionError
parts = file_doc.get_extension()
extension = parts[1]
extension = extension.lstrip(".")
@@ -177,6 +183,8 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
"""called by tree view (to fetch node's children)"""
frappe.has_permission("Chart of Accounts Importer", throw=True)
file_doc, extension = get_file(file_name)
parent = None if parent == _("All Accounts") else parent
@@ -324,6 +332,8 @@ def build_response_as_excel(writer):
@frappe.whitelist()
def download_template(file_type, template_type, company):
frappe.has_permission("Chart of Accounts Importer", throw=True)
writer = get_template(template_type, company)
if file_type == "CSV":
@@ -376,7 +386,6 @@ def get_sample_template(writer, company):
return writer
@frappe.whitelist()
def validate_accounts(file_doc, extension):
if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True)

View File

@@ -4,6 +4,7 @@
"autoname": "field:bank_name",
"creation": "2016-05-04 14:35:00.402544",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cheque-print-template",
"engine": "InnoDB",
"field_order": [
"settings",
@@ -293,6 +294,7 @@
"fieldtype": "HTML"
}
],
"icon": "printer",
"links": [],
"max_attachments": 1,
"modified": "2026-06-08 12:10:35.829531",

View File

@@ -7,6 +7,7 @@
"description": "Track separate Income and Expense for product verticals or divisions.",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cost-center",
"engine": "InnoDB",
"field_order": [
"sb0",
@@ -122,11 +123,11 @@
"label": "Disabled"
}
],
"icon": "fa fa-money",
"icon": "chart-pie",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:15:27.367298",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -181,6 +182,54 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Projects Manager",
"select": 1
},
{
"role": "Projects User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -12,6 +12,19 @@ frappe.treeview_settings["Cost Center"] = {
],
root_label: "Cost Centers",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the number renders as a badge (see onrender)
return frappe.utils.escape_html(node.data.cost_center_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.cost_center_number) {
flags.push(frappe.ui.badge({ label: node.data.cost_center_number }));
}
erpnext.utils.render_tree_node_flags(node, flags);
},
add_tree_node: "erpnext.accounts.utils.add_cc",
menu_items: [
{
@@ -42,6 +55,37 @@ frappe.treeview_settings["Cost Center"] = {
},
],
ignore_fields: ["parent_cost_center"],
toolbar: [
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Cost Center");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Cost Center")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
onload: function (treeview) {
function get_company() {
return treeview.page.fields_dict.company.get_value();
@@ -82,3 +126,22 @@ frappe.treeview_settings["Cost Center"] = {
);
},
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -4,6 +4,7 @@
"autoname": "CC-ALLOC-.#####",
"creation": "2022-01-13 20:07:29.871109",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cost_center_allocation",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -65,6 +66,7 @@
"read_only": 1
}
],
"icon": "chart-pie",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -5,7 +5,10 @@ frappe.ui.form.on("Coupon Code", {
setup: function (frm) {
frm.set_query("pricing_rule", function () {
return {
filters: [["Pricing Rule", "coupon_code_based", "=", "1"]],
filters: {
coupon_code_based: 1,
disable: 0,
},
};
});
},

View File

@@ -5,6 +5,7 @@
"creation": "2018-01-22 14:34:39.701832",
"doctype": "DocType",
"document_type": "Other",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/coupon-code",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -124,8 +125,9 @@
"label": "From External Ecomm Platform"
}
],
"icon": "ticket-percent",
"links": [],
"modified": "2024-11-19 16:35:11.836441",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
@@ -179,6 +181,14 @@
"role": "Website Manager",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
}
],
"sort_field": "creation",

View File

@@ -42,7 +42,23 @@ class CouponCode(Document):
self.coupon_code = frappe.generate_hash()[:10].upper()
def validate(self):
self.validate_from_to_dates("valid_from", "valid_upto")
self.validate_pricing_rule()
if self.coupon_type == "Gift Card":
self.maximum_use = 1
if not self.customer:
frappe.throw(_("Please select the customer."))
def validate_pricing_rule(self):
if not self.pricing_rule or self.from_external_ecomm_platform:
return
# Allow existing coupons to be updated after their pricing rule is disabled.
if not (
self.has_value_changed("pricing_rule") or self.has_value_changed("from_external_ecomm_platform")
):
return
if frappe.db.get_value("Pricing Rule", self.pricing_rule, "disable"):
frappe.throw(_("Pricing Rule {0} is disabled").format(frappe.bold(self.pricing_rule)))

View File

@@ -113,6 +113,43 @@ class TestCouponCode(ERPNextTestSuite):
def setUp(self):
test_create_test_data()
def test_disabled_pricing_rule_validation(self):
coupon = frappe.get_doc("Coupon Code", "SAVE30")
rule = frappe.get_doc("Pricing Rule", coupon.pricing_rule)
rule.disable = 1
rule.save()
with self.subTest("new coupon cannot select a disabled rule"):
new_coupon = frappe.copy_doc(coupon)
new_coupon.coupon_name = "Festival Savings"
new_coupon.coupon_code = "FESTSAVE"
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
new_coupon.insert()
with self.subTest("existing coupon can retain a disabled rule"):
coupon.description = "Offer paused"
coupon.save()
coupon.reload()
self.assertEqual(coupon.description, "Offer paused")
self.assertEqual(coupon.pricing_rule, rule.name)
with self.subTest("existing coupon cannot switch to a disabled rule"):
disabled_rule = frappe.copy_doc(rule)
disabled_rule.insert()
coupon.reload()
coupon.pricing_rule = disabled_rule.name
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
coupon.save()
coupon.reload()
self.assertEqual(coupon.pricing_rule, rule.name)
def test_cannot_save_coupon_with_reversed_validity_dates(self):
coupon = frappe.get_doc("Coupon Code", "SAVE30")
coupon.valid_from = "2026-09-17"
coupon.valid_upto = "2026-09-02"
with self.assertRaises(frappe.exceptions.InvalidDates):
coupon.save()
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)

View File

@@ -2,6 +2,7 @@
"actions": [],
"creation": "2022-01-10 13:03:26.237081",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/currency-exchange-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -101,6 +102,7 @@
"label": "Use HTTP Protocol"
}
],
"icon": "refresh-cw",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -4,6 +4,7 @@
"autoname": "naming_series:",
"creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/dunning",
"engine": "InnoDB",
"field_order": [
"naming_series",
@@ -397,6 +398,7 @@
"fieldtype": "Column Break"
}
],
"icon": "bell-ring",
"is_submittable": 1,
"links": [],
"modified": "2026-05-30 23:18:04.712528",

View File

@@ -100,6 +100,7 @@
"fieldtype": "Column Break"
}
],
"icon": "bell",
"links": [
{
"link_doctype": "Dunning",

View File

@@ -4,6 +4,7 @@
"autoname": "ACC-ERR-.YYYY.-.#####",
"creation": "2018-04-13 18:25:55.943587",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/exchange-rate-revaluation",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -107,6 +108,7 @@
"precision": "9"
}
],
"icon": "arrow-right-left",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:09:42.951164",

View File

@@ -5,6 +5,7 @@
"creation": "2018-04-13 17:42:43.252224",
"doctype": "DocType",
"document_type": "Document",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/finance-book",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -18,9 +19,9 @@
"unique": 1
}
],
"icon": "fa fa-book",
"icon": "book",
"links": [],
"modified": "2024-03-27 13:09:44.514241",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Finance Book",
@@ -55,6 +56,18 @@
"report": 1,
"role": "Auditor",
"share": 1
},
{
"role": "HR Manager",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -236,6 +236,8 @@ async function refresh_tree_view(dialog, account_rows) {
parent: wrapper,
label: company,
root_value: company,
// read-only preview: row-mode visuals without actions
row_style: true,
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
toolbar: [],

View File

@@ -3,6 +3,7 @@
"autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/financial-report-template",
"engine": "InnoDB",
"field_order": [
"template_name",
@@ -64,6 +65,7 @@
}
],
"grid_page_length": 50,
"icon": "file-spreadsheet",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-23 01:04:05.797161",

View File

@@ -6,6 +6,7 @@
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/fiscal-year",
"engine": "InnoDB",
"field_order": [
"year",
@@ -79,10 +80,10 @@
"set_only_once": 1
}
],
"icon": "fa fa-calendar",
"icon": "calendar",
"idx": 1,
"links": [],
"modified": "2024-05-27 17:29:55.560840",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
@@ -131,6 +132,10 @@
{
"read": 1,
"role": "Auditor"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"show_name_in_global_search": 1,

View File

@@ -376,7 +376,7 @@
"precision": "9"
}
],
"icon": "fa fa-list",
"icon": "book-open",
"idx": 1,
"in_create": 1,
"links": [],

View File

@@ -5,6 +5,7 @@
"autoname": "ACC-INV-DISC-.YYYY.-.#####",
"creation": "2019-03-07 12:01:56.296952",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/invoice_discounting",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -169,6 +170,7 @@
"read_only": 1
}
],
"icon": "ticket-percent",
"is_submittable": 1,
"links": [],
"modified": "2026-09-09 17:04:59.512294",

View File

@@ -5,6 +5,7 @@
"creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/item-tax-template",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -56,8 +57,9 @@
"fieldtype": "Section Break"
}
],
"icon": "circle-percent",
"links": [],
"modified": "2024-03-27 13:09:55.573483",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -95,6 +97,54 @@
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
}
],
"show_name_in_global_search": 1,

View File

@@ -6,6 +6,7 @@
"creation": "2022-01-25 10:29:58.717206",
"doctype": "DocType",
"document_type": "Document",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/journal-entry",
"engine": "InnoDB",
"field_order": [
"entry_type_and_date",
@@ -661,7 +662,7 @@
"label": "Custom Remark"
}
],
"icon": "fa fa-file-text",
"icon": "notebook-pen",
"idx": 176,
"is_submittable": 1,
"links": [

View File

@@ -1356,6 +1356,10 @@ def get_default_bank_cash_account(
):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
# `select`, not `read`: this also runs server-side from get_payment_entry, and Auditor/HR User/Desk
# User hold only the select row on Company. doc= brings User Permissions to bear.
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
if mode_of_payment:
account = get_bank_cash_account(mode_of_payment, company).get("account")
@@ -1384,6 +1388,10 @@ def get_default_bank_cash_account(
account = account_list[0].name
if account:
# `fetch_balance` is caller supplied, so authorise the account here rather than relying on
# get_balance_on(), which only checks on the branch that reads a balance.
frappe.has_permission("Account", doc=account, throw=True)
account_details = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"], as_dict=1
)
@@ -1568,30 +1576,39 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
if not frappe.db.has_column("Journal Entry", searchfield):
return []
JournalEntry = frappe.qb.DocType("Journal Entry")
JournalEntryAccount = frappe.qb.DocType("Journal Entry Account")
account = filters.get("account")
party = filters.get("party")
query = (
frappe.qb.from_(JournalEntry)
.join(JournalEntryAccount)
.on(JournalEntryAccount.parent == JournalEntry.name)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
.where(JournalEntryAccount.account == filters.get("account"))
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
.where(JournalEntry.docstatus == 1)
.where(JournalEntry[searchfield].like(f"%{txt}%"))
.orderby(JournalEntry.name, order=frappe.qb.desc)
.limit(page_len)
.offset(start)
# each names one value: a list would be read as a filter operator and widen the search.
for value in (account, party):
if value and not isinstance(value, str):
frappe.throw(_("Invalid filter"), frappe.PermissionError)
# get_list applies the permission query conditions; the child-table filter resolves the check to `read`
je_filters = [
["docstatus", "=", 1],
[searchfield, "like", f"%{txt}%"],
["Journal Entry Account", "account", "=", account],
["Journal Entry Account", "reference_type", "is", "not set"],
]
je_filters.append(
["Journal Entry Account", "party", "=", party]
if party
else ["Journal Entry Account", "party", "is", "not set"]
)
party = filters.get("party")
if party:
query = query.where(JournalEntryAccount.party == party)
else:
query = query.where(JournalEntryAccount.party.isnull() | (JournalEntryAccount.party == ""))
return query.run()
return frappe.get_list(
"Journal Entry",
filters=je_filters,
fields=["name", "posting_date", "remark"],
order_by="name desc",
limit_start=start,
limit_page_length=page_len,
as_list=True,
# one row per entry, not per matching account row. group_by rather than distinct: frappe
# drops ORDER BY from a distinct query on postgres, which would lose the ordering above.
group_by="name",
)
@frappe.whitelist()
@@ -1810,6 +1827,10 @@ def make_reverse_journal_entry(source_name, target_doc=None):
def post_process(source, target):
target.reversal_of = source.name
target.naming_series = source.naming_series
if source.voucher_type == "Bank Entry":
target.cheque_no = source.cheque_no
target.cheque_date = source.cheque_date
doclist = get_mapped_doc(
"Journal Entry",

View File

@@ -4,6 +4,7 @@
"creation": "2020-04-09 01:32:51.332301",
"doctype": "DocType",
"document_type": "Document",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/journal-entry-template",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -85,6 +86,7 @@
"label": "Multi Currency"
}
],
"icon": "notebook-text",
"links": [],
"modified": "2024-03-27 13:09:58.814734",
"modified_by": "Administrator",

View File

@@ -40,6 +40,7 @@
"label": "General and Payment Ledger mismatch"
}
],
"icon": "heart-pulse",
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],

View File

@@ -57,6 +57,7 @@
}
],
"hide_toolbar": 1,
"icon": "activity",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -92,6 +92,7 @@
}
],
"hide_toolbar": 1,
"icon": "merge",
"links": [],
"modified": "2024-03-27 13:10:02.198009",
"modified_by": "Administrator",

View File

@@ -97,6 +97,7 @@
"label": "Discretionary Reason"
}
],
"icon": "star",
"in_create": 1,
"links": [],
"modified": "2024-07-01 08:51:13.927009",

View File

@@ -8,7 +8,7 @@ frappe.ui.form.on("Loyalty Program", {
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
${__("Notes")}
</h4>
<ul>

View File

@@ -3,6 +3,7 @@
"autoname": "field:loyalty_program_name",
"creation": "2018-01-23 06:23:05.731431",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/loyalty-program",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -153,8 +154,9 @@
"options": "Project"
}
],
"icon": "gift",
"links": [],
"modified": "2024-03-27 13:10:03.361383",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Program",
@@ -171,6 +173,14 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -96,6 +96,8 @@ def get_loyalty_program_details_with_points(
include_expired_entry=False,
current_transaction_amount=0,
):
frappe.has_permission("Customer", doc=customer, throw=True)
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
loyalty_details = get_loyalty_details(
@@ -126,6 +128,10 @@ def get_loyalty_program_details(
silent=False,
include_expired_entry=False,
):
# The customer is what the caller is entitled to, not the programme: a check on Loyalty Program
# would be read-only to System Manager and deny every role that fills in the calling forms.
frappe.has_permission("Customer", doc=customer, throw=True)
lp_details = frappe._dict()
if not loyalty_program:
@@ -147,6 +153,13 @@ def get_loyalty_program_details(
@frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None):
# both call sites send only `loyalty_program`, so the calling form is the boundary; the customer branch stays guarded
if not (frappe.has_permission("Sales Invoice") or frappe.has_permission("POS Invoice")):
frappe.throw(_("Not permitted"), frappe.PermissionError)
if customer:
frappe.has_permission("Customer", doc=customer, throw=True)
customer_loyalty_program = None
if not loyalty_program:
customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")

View File

@@ -6,6 +6,7 @@
"creation": "2012-12-04 17:49:20",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/mode-of-payment",
"engine": "InnoDB",
"field_order": [
"mode_of_payment",
@@ -44,11 +45,11 @@
"label": "Enabled"
}
],
"icon": "fa fa-credit-card",
"icon": "wallet",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-14 18:16:47.795986",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
@@ -76,6 +77,30 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -43,10 +43,10 @@
"options": "Monthly Distribution Percentage"
}
],
"icon": "fa fa-bar-chart",
"icon": "chart-bar",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:10:05.873547",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
@@ -69,6 +69,10 @@
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"sort_field": "creation",

View File

@@ -3,6 +3,7 @@
"allow_copy": 1,
"creation": "2017-08-29 02:22:54.947711",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/opening-invoice-creation-tool",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -87,6 +88,7 @@
}
],
"hide_toolbar": 1,
"icon": "file-plus",
"issingle": 1,
"links": [],
"modified": "2026-05-30 23:18:48.691227",

View File

@@ -50,6 +50,7 @@
"options": "secondary_role"
}
],
"icon": "link",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:10:08.607170",

View File

@@ -422,21 +422,17 @@ frappe.ui.form.on("Payment Entry", {
show_general_ledger: function (frm) {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
"fa fa-table"
);
frm.add_custom_button(__("Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
});
}
},

View File

@@ -5,6 +5,7 @@
"autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-entry",
"engine": "InnoDB",
"field_order": [
"type_of_payment",
@@ -784,6 +785,7 @@
}
],
"grid_page_length": 50,
"icon": "banknote",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [

View File

@@ -84,6 +84,7 @@
"reqd": 1
}
],
"icon": "credit-card",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-14 16:49:55.210352",

View File

@@ -5,6 +5,7 @@
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"icon": "book-open",
"field_order": [
"posting_date",
"company",

View File

@@ -3,6 +3,7 @@
"autoname": "naming_series:",
"creation": "2018-07-20 16:43:08.505978",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-order",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -107,6 +108,7 @@
"label": "Account"
}
],
"icon": "send",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:10.186727",

View File

@@ -57,26 +57,56 @@ class PaymentOrder(Document):
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
def _readable_payment_order(filters):
"""Authorise the parent before reading its rows.
A child table carries no permissions of its own, so a read of it has to be authorised on the
Payment Order the rows belong to.
"""
parent = filters.get("parent")
if not parent or not frappe.db.exists("Payment Order", parent):
return None
ptype = "select" if frappe.only_has_select_perm("Payment Order") else "read"
frappe.has_permission("Payment Order", ptype, doc=parent, throw=True)
return parent
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
if not _readable_payment_order(filters):
return []
return frappe.get_all(
"Payment Order Reference",
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
fields=["mode_of_payment"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
if not _readable_payment_order(filters):
return []
return frappe.get_all(
"Payment Order Reference",
filters={
"parent": filters.get("parent"),
"supplier": ["like", f"%{txt}%"],
"payment_reference": ["is", "not set"],
},
fields=["supplier"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
)

View File

@@ -3,6 +3,7 @@
"allow_copy": 1,
"creation": "2014-07-09 12:04:51.681583",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-reconciliation",
"engine": "InnoDB",
"field_order": [
"company",
@@ -235,7 +236,7 @@
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"icon": "arrow-left-right",
"is_virtual": 1,
"issingle": 1,
"links": [],

View File

@@ -3,6 +3,7 @@
"autoname": "naming_series:",
"creation": "2015-12-15 22:23:24.745065",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-request",
"engine": "InnoDB",
"field_order": [
"payment_request_type",
@@ -474,6 +475,7 @@
}
],
"grid_page_length": 50,
"icon": "hand-coins",
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,

View File

@@ -5,6 +5,7 @@
"autoname": "field:payment_term_name",
"creation": "2017-08-10 15:24:54.876365",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-terms",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -115,8 +116,9 @@
"fieldtype": "Column Break"
}
],
"icon": "calendar-clock",
"links": [],
"modified": "2024-03-27 13:10:11.511137",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
@@ -157,6 +159,30 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -5,6 +5,7 @@
"autoname": "field:template_name",
"creation": "2017-08-10 15:34:28.058054",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-terms-template",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -34,6 +35,7 @@
"label": "Allocate Payment Based On Payment Terms"
}
],
"icon": "calendar-clock",
"links": [],
"modified": "2024-04-07 11:26:42.021585",
"modified_by": "Administrator",

View File

@@ -20,6 +20,7 @@
}
],
"grid_page_length": 50,
"icon": "anchor",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -41,21 +41,17 @@ frappe.ui.form.on("Period Closing Voucher", {
refresh: function (frm) {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
"fa fa-table"
);
frm.add_custom_button(__("Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
});
}
},
});

View File

@@ -3,6 +3,7 @@
"autoname": "ACC-PCV-.YYYY.-.#####",
"creation": "2013-01-10 16:34:07",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/period-closing-voucher",
"engine": "InnoDB",
"field_order": [
"transaction_date",
@@ -110,7 +111,7 @@
"reqd": 1
}
],
"icon": "fa fa-file-text",
"icon": "folder-lock",
"idx": 1,
"is_submittable": 1,
"links": [],

View File

@@ -254,6 +254,7 @@
}
],
"grid_page_length": 50,
"icon": "book-lock",
"is_submittable": 1,
"links": [
{

View File

@@ -254,6 +254,13 @@ class POSClosingEntry(StatusUpdater):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
pos_profile = filters.get("parent")
if not pos_profile or not frappe.db.exists("POS Profile", pos_profile):
return []
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
frappe.has_permission("POS Profile", ptype, doc=pos_profile, throw=True)
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1)
return [c for c in cashiers_list]

View File

@@ -1634,10 +1634,10 @@
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"icon": "receipt-text",
"is_submittable": 1,
"links": [],
"modified": "2026-08-12 12:00:00.000000",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1680,6 +1680,14 @@
"permlevel": 1,
"read": 1,
"role": "All"
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -899,6 +899,19 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
# select-or-read on POS Profile: a bare check defaults to `read`, which Sales Manager lacks.
# The profile is the only boundary that fits -- Item and Bin read both exclude Accounts Manager.
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
frappe.has_permission("POS Profile", ptype, throw=True)
# the caller picks the warehouse when allow_warehouse_change is set; costs nobody who has no
# Warehouse User Permission
from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions
if warehouse_permissions := get_user_permissions(frappe.session.user).get("Warehouse"):
allowed_warehouses = get_allowed_docs_for_doctype(warehouse_permissions, "POS Invoice")
if allowed_warehouses and warehouse not in allowed_warehouses:
frappe.throw(_("Not permitted for {0}").format(warehouse), frappe.PermissionError)
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)

View File

@@ -126,6 +126,7 @@
"reqd": 1
}
],
"icon": "merge",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -9,6 +9,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder import DocType
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -16,7 +17,6 @@ from frappe.utils.scheduler import is_scheduler_inactive
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
class POSInvoiceMergeLog(Document):
@@ -214,6 +214,8 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
reversed_rows = get_reversed_rows([doc.return_against for doc in data if doc.is_return])
for doc in data:
old_new_item_map = frappe._dict()
old_new_tax_map = frappe._dict()
@@ -238,9 +240,13 @@ class POSInvoiceMergeLog(Document):
si_item.pos_invoice = doc.name
si_item.pos_invoice_item = item.name
if doc.is_return:
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
doc.return_against, item.pos_invoice_item
)
reversed_row = reversed_rows.get(item.pos_invoice_item) or frappe._dict()
si_item.sales_invoice_item = reversed_row.get("name")
# quote the rate of the row being reversed: rounding an invoice-level discount
# can leave a return's net rate a minor unit above the sale's, and
# validate_returned_items refuses a return priced above its original
if si_item.sales_invoice_item:
si_item.rate = reversed_row.rate
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item)
@@ -432,6 +438,28 @@ class POSInvoiceMergeLog(Document):
si.cancel()
def get_reversed_rows(return_against):
"""Rows of the consolidated sales these returns reverse, keyed by the POS invoice row."""
if not return_against:
return {}
sales_invoice = DocType("Sales Invoice")
sales_invoice_item = DocType("Sales Invoice Item")
rows = (
frappe.qb.from_(sales_invoice)
.from_(sales_invoice_item)
.select(sales_invoice_item.name, sales_invoice_item.rate, sales_invoice_item.pos_invoice_item)
.where(
(sales_invoice.name == sales_invoice_item.parent)
& (sales_invoice.is_return == 0)
& (sales_invoice_item.pos_invoice.isin(return_against))
)
).run(as_dict=True)
return {row.pos_invoice_item: row for row in rows}
def get_all_unconsolidated_invoices():
filters = {
"consolidated_invoice": ["in", ["", None]],

View File

@@ -1,8 +1,10 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
from contextlib import contextmanager
import frappe
from frappe.utils import flt
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
@@ -21,6 +23,67 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestSuite
@contextmanager
def rounding_method(method):
"""System Settings is also cached on frappe.local, so that copy has to go as well."""
previous = frappe.db.get_single_value("System Settings", "rounding_method")
try:
frappe.db.set_single_value("System Settings", "rounding_method", method)
frappe.local.system_settings = None
yield
finally:
frappe.db.set_single_value("System Settings", "rounding_method", previous)
frappe.local.system_settings = None
def sell_over_the_counter(lines, discount_percentage=0):
item_code, qty, rate = lines[0]
sale = create_pos_invoice(item_code=item_code, qty=qty, rate=rate, do_not_save=True)
for item_code, qty, rate in lines[1:]:
sale.append(
"items",
{
"item_code": item_code,
"qty": qty,
"rate": rate,
"price_list_rate": rate,
"warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
if discount_percentage:
sale.apply_discount_on = "Net Total"
sale.additional_discount_percentage = discount_percentage
sale.run_method("calculate_taxes_and_totals")
payable = sale.rounded_total or sale.grand_total
sale.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": payable})
sale.paid_amount = sale.base_paid_amount = payable
sale.insert()
sale.submit()
return sale
def refund_over_the_counter(sale, qty=None):
"""Hand back every line of `sale`, `qty` of each when fewer units come back."""
note = make_sales_return(sale.name)
if qty is not None:
for item in note.items:
item.qty = qty
note.run_method("calculate_taxes_and_totals")
refundable = note.rounded_total or note.grand_total
note.payments[0].amount = refundable
for spare in note.payments[1:]:
spare.amount = 0
note.paid_amount = note.base_paid_amount = refundable
note.insert()
note.submit()
return note
class TestPOSInvoiceMergeLog(ERPNextTestSuite):
def setUp(self):
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
@@ -479,3 +542,81 @@ class TestPOSInvoiceMergeLog(ERPNextTestSuite):
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
)
self.assertEqual(pos_merge_log_company, closing_entry.company)
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_consolidating_returns_priced_off_a_rounded_invoice_discount(self):
"""A return works out its own share of an invoice-level discount, so rounding can leave
it a minor unit above the sale's, and validate_returned_items then refuses it.
Every shape that reaches a consolidated credit note goes through one closing entry:
a split landing on a half minor unit, the same item on two rows so the rows can only
be paired through sales_invoice_item, fewer units coming back than went out, and — as
a control — a sale with no invoice-level discount to split at all.
"""
for item_code in ("_Test Item", "_Test Item 2"):
make_stock_entry(to_warehouse="_Test Warehouse - _TC", item_code=item_code, rate=100, qty=40)
with rounding_method("Banker's Rounding (legacy)"):
tied = sell_over_the_counter(
[("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)], discount_percentage=25
)
repeated = sell_over_the_counter(
[("_Test Item", 1, 42.86), ("_Test Item", 1, 57.14)], discount_percentage=25
)
oversold = sell_over_the_counter(
[("_Test Item", 3, 42.86), ("_Test Item 2", 3, 57.14)], discount_percentage=25
)
undiscounted = sell_over_the_counter([("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)])
# the sale and the return really do round the split apart
self.assertEqual(
{item.item_code: item.net_rate for item in tied.items},
{"_Test Item": 32.15, "_Test Item 2": 42.85},
)
returns = [
refund_over_the_counter(tied),
refund_over_the_counter(repeated),
refund_over_the_counter(oversold, qty=-1),
refund_over_the_counter(undiscounted),
]
self.assertEqual(
{item.item_code: item.net_rate for item in returns[0].items},
{"_Test Item": 32.14, "_Test Item 2": 42.86},
)
self.make_closing_entry()
for pos_invoice in [tied, repeated, oversold, undiscounted, *returns]:
pos_invoice.load_from_db()
self.assertTrue(
frappe.db.exists("Sales Invoice", pos_invoice.consolidated_invoice),
f"{pos_invoice.name} was not consolidated",
)
self.assertEqual(
frappe.db.get_value("Sales Invoice", pos_invoice.consolidated_invoice, "outstanding_amount"),
0,
)
for note in returns:
# no returned row may be priced above the row it reverses
for row in frappe.get_all(
"Sales Invoice Item",
filters={"parent": note.consolidated_invoice},
fields=["item_code", "rate", "sales_invoice_item"],
):
self.assertTrue(row.sales_invoice_item, f"{row.item_code} lost its link to the sale")
sold_rate = frappe.db.get_value("Sales Invoice Item", row.sales_invoice_item, "rate")
self.assertLessEqual(row.rate, sold_rate)
# returns for one customer land on a single credit note, which still adds up to
# everything handed back over the counter
refunded = {}
for note in returns:
refunded[note.consolidated_invoice] = refunded.get(note.consolidated_invoice, 0) + flt(
note.grand_total
)
for consolidated_name, handed_back in refunded.items():
self.assertEqual(
flt(frappe.db.get_value("Sales Invoice", consolidated_name, "grand_total"), 2),
flt(handed_back, 2),
)

View File

@@ -129,6 +129,7 @@
"reqd": 1
}
],
"icon": "folder-open",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:16.034296",

View File

@@ -4,6 +4,7 @@
"autoname": "Prompt",
"creation": "2013-05-24 12:15:51",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/pos-profile",
"engine": "InnoDB",
"field_order": [
"company",
@@ -545,7 +546,7 @@
}
],
"grid_page_length": 50,
"icon": "icon-cog",
"icon": "monitor",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [
@@ -570,7 +571,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-05-26 12:07:48.597412",
"modified": "2026-09-16 11:42:07.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -594,6 +595,10 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "Sales Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -51,6 +51,7 @@
}
],
"hide_toolbar": 0,
"icon": "settings",
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:19.677217",

View File

@@ -40,7 +40,7 @@ frappe.ui.form.on("Pricing Rule", {
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
${__("Notes")}
</h4>
<ul>
@@ -63,7 +63,7 @@ frappe.ui.form.on("Pricing Rule", {
</ul>
</td></tr>
<tr><td>
<h4><i class="fa fa-question-sign"></i>
<h4><svg class="icon icon-sm"><use href="#icon-circle-question-mark"></use></svg>
${__("How Pricing Rule is applied?")}
</h4>
<ol>
@@ -108,7 +108,7 @@ frappe.ui.form.on("Pricing Rule", {
</td></tr>
</table>`;
frm.set_df_property("pricing_rule_help", "options", help_content);
frm.get_field("pricing_rule_help").html(help_content);
frm.events.set_options_for_applicable_for(frm);
frm.trigger("toggle_reqd_apply_on");
},

View File

@@ -5,6 +5,7 @@
"autoname": "naming_series:",
"creation": "2014-02-21 15:02:51",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/pricing-rule",
"engine": "InnoDB",
"field_order": [
"applicability_section",
@@ -654,7 +655,7 @@
"label": "Don't Enforce Free Item Qty"
}
],
"icon": "fa fa-gift",
"icon": "badge-percent",
"idx": 1,
"links": [],
"modified": "2026-02-17 12:24:07.553505",

View File

@@ -12,6 +12,29 @@ from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import cint, flt
# the transactions the pricing engine is called for, from transaction.js and the POS
PRICING_TRANSACTION_DOCTYPES = frozenset(
{
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
"Material Request",
# these three also extend a controller that calls the pricing engine: BOM and BOM Creator
# through TransactionController, Request for Quotation through BuyingController
"BOM",
"BOM Creator",
"Request for Quotation",
# no client sends this one, but set_transaction_type below still branches on it
"Opportunity",
}
)
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
other_fields = ["other_item_code", "other_item_group", "other_brand"]
@@ -364,6 +387,18 @@ def apply_pricing_rule(args, doc=None):
args = frappe._dict(args)
# The transaction being priced decides who may price it; doc= where the caller named one.
# An allow-list, not a type check: any readable doctype would otherwise satisfy has_permission.
transaction_doctype = args.get("doctype")
if transaction_doctype not in PRICING_TRANSACTION_DOCTYPES:
frappe.throw(_("Invalid doctype"), frappe.PermissionError)
transaction_name = args.get("name")
if not isinstance(transaction_name, str) or not frappe.db.exists(transaction_doctype, transaction_name):
transaction_name = None
frappe.has_permission(transaction_doctype, doc=transaction_name, throw=True)
set_transaction_type(args)
# list of dictionaries
@@ -389,6 +424,7 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
set_transaction_type(args_copy)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
@@ -722,14 +758,18 @@ def set_transaction_type(pricing_ctx: frappe._dict) -> None:
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
items = [filters.get("value")]
if filters.get("apply_on") != "Item Code":
field = frappe.scrub(filters.get("apply_on"))
items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get("value")})]
if filters.get("apply_on") == "Item Code":
item_filters = [["name", "=", filters.get("value")]]
else:
item_filters = [[frappe.scrub(filters.get("apply_on")), "=", filters.get("value")]]
items = frappe.get_list("Item", filters=item_filters, pluck="name")
if not items:
return []
return frappe.get_all(
"UOM Conversion Detail",
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
filters={"parent": ("in", items), "parenttype": "Item", "uom": ("like", f"{txt}%")},
fields=["uom"],
as_list=1,
distinct=True,

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