Compare commits

..

130 Commits

Author SHA1 Message Date
MochaMind
af63718c1b fix: Esperanto translations 2026-06-29 01:34:09 +05:30
MochaMind
442825b4f1 fix: Serbian (Latin) translations 2026-06-29 01:34:04 +05:30
MochaMind
b13d7f3ef3 fix: Norwegian Bokmal translations 2026-06-29 01:34:00 +05:30
MochaMind
4f7633e722 fix: Bosnian translations 2026-06-29 01:33:56 +05:30
MochaMind
f2a864f7bb fix: Burmese translations 2026-06-29 01:33:52 +05:30
MochaMind
70cf3a91e9 fix: Hindi translations 2026-06-29 01:33:47 +05:30
MochaMind
ff5a4e0cf2 fix: Croatian translations 2026-06-29 01:33:43 +05:30
MochaMind
621d67f8eb fix: Thai translations 2026-06-29 01:33:39 +05:30
MochaMind
8e51d1b5fc fix: Indonesian translations 2026-06-29 01:33:35 +05:30
MochaMind
bd7cce66e5 fix: Portuguese, Brazilian translations 2026-06-29 01:33:31 +05:30
MochaMind
922b4b8c79 fix: Vietnamese translations 2026-06-29 01:33:27 +05:30
MochaMind
eab4da6189 fix: Chinese Simplified translations 2026-06-29 01:33:23 +05:30
MochaMind
a5516aa4a0 fix: Turkish translations 2026-06-29 01:33:19 +05:30
MochaMind
5065d4b28d fix: Swedish translations 2026-06-29 01:33:14 +05:30
MochaMind
b45e71c793 fix: Serbian (Cyrillic) translations 2026-06-29 01:33:09 +05:30
MochaMind
121cbbf694 fix: Slovenian translations 2026-06-29 01:33:06 +05:30
MochaMind
93ceb15d4e fix: Russian translations 2026-06-29 01:33:01 +05:30
MochaMind
f0c6b4002b fix: Portuguese translations 2026-06-29 01:32:57 +05:30
MochaMind
32f7df8fc4 fix: Polish translations 2026-06-29 01:32:53 +05:30
MochaMind
ba09b06fa7 fix: Dutch translations 2026-06-29 01:32:49 +05:30
MochaMind
d9fae790ef fix: Korean translations 2026-06-29 01:32:45 +05:30
MochaMind
cafe35ef2e fix: Italian translations 2026-06-29 01:32:41 +05:30
MochaMind
2a9bed4893 fix: Hungarian translations 2026-06-29 01:32:37 +05:30
MochaMind
4094fed5ca fix: German translations 2026-06-29 01:32:33 +05:30
MochaMind
10ddf735a0 fix: Czech translations 2026-06-29 01:32:29 +05:30
MochaMind
6cb03ea530 fix: Bulgarian translations 2026-06-29 01:32:25 +05:30
MochaMind
b06ce350e7 fix: Arabic translations 2026-06-29 01:32:20 +05:30
MochaMind
69c1e0efd8 fix: Spanish translations 2026-06-29 01:32:16 +05:30
MochaMind
efc4da4bb2 fix: Uzbek translations 2026-06-29 01:32:12 +05:30
MochaMind
3b55de02ac fix: Persian translations 2026-06-29 01:32:08 +05:30
MochaMind
838ee003f1 fix: Danish translations 2026-06-29 01:32:03 +05:30
MochaMind
7fd5dd3ac5 fix: French translations 2026-06-29 01:31:59 +05:30
MochaMind
d8c267c253 fix: sync translations from crowdin (#56590) 2026-06-28 21:29:24 +02:00
MochaMind
56926ffe00 chore: update POT file (#56592) 2026-06-28 21:28:18 +02:00
rohitwaghchaure
6c38856f65 feat: Standard Valuation Rate (#56570)
* feat: standard rate valuation

* fix: greptile comments

* fix: PPV account should be mandatory for standard cost valuation
2026-06-28 20:35:01 +05:30
Mihir Kandoi
081fbe9e3f Merge pull request #56586 from mihir-kandoi/alias-no-copy
fix: party aliases should be no copy
2026-06-27 16:24:40 +05:30
Mihir Kandoi
057af21cd8 fix: party aliases should be no copy 2026-06-27 16:13:43 +05:30
rohitwaghchaure
c7ef42ef98 fix: sync Stock Reconciliation difference amount with GL after reposting (#56574)
* fix: sync Stock Reconciliation difference amount with GL after reposting

* fix: placement of recalculate differece amount function
2026-06-27 10:28:45 +00:00
Diptanil Saha
79ad11e21b chore(crm_settings): remove unused delete_custom_fields import (#56558) 2026-06-27 14:38:59 +05:30
Diptanil Saha
485e9041de chore: removing controllers from pre-commit eslint hooks exclude list (#56575)
* chore: removed `controllers` from exclude list on `.pre-commit-config.yaml`

* chore: fix `transactions.js` eslint issues

* chore: fix `taxes_and_totals.js` eslint issue

* chore: fix `accounts.js` eslint issue
2026-06-27 00:42:34 +05:30
rohitwaghchaure
5e60e4faa7 fix: do not allow closing the accounting period for future dates (#56551) 2026-06-26 17:28:20 +00:00
Nabin Hait
3f053e599c Merge pull request #56536 from frappe/chore/test-bom-search
test: BOM Search report coverage
2026-06-26 22:01:25 +05:30
Nabin Hait
b0232f41ea Merge pull request #56545 from frappe/chore/test-incorrect-stock-value-report
test: Incorrect Stock Value Report report coverage
2026-06-26 22:00:43 +05:30
Nabin Hait
1d517375d9 Merge pull request #56553 from frappe/chore/refactor-ar-ap-report-tests
test: reuse bootstrap master data in Accounts Receivable/Payable report tests
2026-06-26 22:00:15 +05:30
Nabin Hait
1d9d982719 Merge pull request #56554 from frappe/chore/refactor-cash-flow-report-tests
test: reuse bootstrap master data in Cash Flow report tests
2026-06-26 21:59:47 +05:30
Nabin Hait
66a711c849 Merge pull request #56555 from frappe/chore/refactor-general-ledger-report-tests
test: reuse bootstrap master data in General Ledger report tests
2026-06-26 21:58:39 +05:30
Nabin Hait
a2f201e1d7 Merge pull request #56556 from frappe/chore/refactor-stock-balance-report-tests
test: reuse bootstrap master data in Stock Balance report tests
2026-06-26 21:58:05 +05:30
Nabin Hait
b989bef967 Merge pull request #56557 from frappe/chore/refactor-stock-ledger-projected-report-tests
test: reuse bootstrap master data in Stock Ledger & Stock Projected Qty report tests
2026-06-26 21:57:30 +05:30
Nabin Hait
c1b91b0f5f Merge pull request #56541 from frappe/chore/test-item-where-used
test: Item Where Used report coverage
2026-06-26 21:56:33 +05:30
rohitwaghchaure
31f89b72b4 fix: ignored posting time 00:00:00 in RIV (#56571) 2026-06-26 13:49:16 +00:00
Diptanil Saha
1e2adc0706 ci: bump pre-commit actions to v3.0.1 (#56562) 2026-06-26 11:14:57 +00:00
Mihir Kandoi
05e7317375 Merge pull request #56559 from Shllokkk/serial-batch-bundle-print-none-fix
fix: remove dead bundle helper call from purchase receipt print format
2026-06-26 16:37:09 +05:30
Mihir Kandoi
c5700f5df4 Merge pull request #56467 from mihir-kandoi/messages/stock
chore: rewrite user-facing messages in stock module
2026-06-26 15:29:32 +05:30
Shllokkk
b9f5a77fa7 fix: remove dead bundle helper call from purchase receipt print format 2026-06-26 15:00:54 +05:30
Nabin Hait
1ffdfbe86e Merge pull request #56539 from frappe/chore/test-delayed-order-report
test: Delayed Order Report report coverage
2026-06-26 14:58:10 +05:30
Nabin Hait
c3ae7a0b95 Merge pull request #56538 from frappe/chore/test-delayed-item-report
test: Delayed Item Report report coverage
2026-06-26 14:57:51 +05:30
Nabin Hait
0458446a06 test: cover search_sub_assemblies filter in BOM Search report 2026-06-26 14:57:26 +05:30
Nabin Hait
99e85bfd82 Merge pull request #56537 from frappe/chore/test-batch-item-expiry-status
test: Batch Item Expiry Status report coverage
2026-06-26 14:56:31 +05:30
Nabin Hait
dca5d2ca3a Merge pull request #56523 from frappe/chore/test-stock-ledger-invariant-check
test: Stock Ledger Invariant Check report coverage
2026-06-26 14:38:21 +05:30
Nabin Hait
dc59ee8034 Merge pull request #56533 from frappe/chore/test-item-variant-details
test: Item Variant Details report coverage
2026-06-26 14:36:58 +05:30
Nabin Hait
76da65ab4c Merge pull request #56531 from frappe/chore/test-item-price-stock
test: Item Price Stock report coverage
2026-06-26 14:36:25 +05:30
Nabin Hait
bde23492fb Merge pull request #56528 from frappe/chore/test-serial-no-ledger
test: Serial No Ledger report coverage
2026-06-26 14:34:51 +05:30
Nabin Hait
d47bc64576 Merge pull request #56527 from frappe/chore/test-serial-and-batch-summary
test: Serial and Batch Summary report coverage
2026-06-26 14:33:55 +05:30
Nabin Hait
4ac863d653 Merge pull request #56515 from frappe/chore/test-batch-wise-balance-history
test: Batch-Wise Balance History report coverage
2026-06-26 14:29:30 +05:30
Nabin Hait
5fe84305fc Merge pull request #56503 from frappe/chore/trial-balance-for-party-test-coverage
test: Trial Balance for Party report coverage
2026-06-26 14:28:04 +05:30
Nabin Hait
a001a15312 test: reuse BootStrapTestData master data in Stock Ledger & Stock Projected Qty report tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:09:20 +05:30
Nabin Hait
7f01d6b24e test: reuse BootStrapTestData master data in Stock Balance report tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:09:13 +05:30
Nabin Hait
f474c10f89 test: reuse BootStrapTestData master data in General Ledger report tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:09:06 +05:30
Nabin Hait
a797c31b57 test: reuse BootStrapTestData master data in Cash Flow report tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:09:00 +05:30
Nabin Hait
1a820abe3c test: reuse BootStrapTestData master data in Accounts Receivable/Payable report tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:08:53 +05:30
Mihir Kandoi
a972ef313a Merge pull request #56127 from harisansari008/fix/qip-allow-rename-develop
fix: allow rename for Quality Inspection Parameter
2026-06-26 13:43:40 +05:30
Nabin Hait
245925815e test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:26 +05:30
Nabin Hait
7f5f2ccfa3 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:05 +05:30
Nabin Hait
dc4f5ce0ab test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:56 +05:30
Nabin Hait
182ef8a8e8 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:48 +05:30
Nabin Hait
b11a2c3e9f test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:19 +05:30
Nabin Hait
67ac8f64e8 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:14 +05:30
Nabin Hait
9158d5f893 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:04 +05:30
Nabin Hait
2aff857561 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:00 +05:30
Nabin Hait
0e6f50ca24 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:28 +05:30
Nabin Hait
6ceddd7a83 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:24 +05:30
Nabin Hait
f2d64d1a2a test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:14 +05:30
Nabin Hait
293ca4e96f test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:08 +05:30
Mihir Kandoi
05e44ca63a Merge pull request #56546 from mihir-kandoi/pg-rawsql-to-orm
refactor: convert convertible raw frappe.db.sql to ORM
2026-06-26 12:21:25 +05:30
Mihir Kandoi
0583349ae4 test: convert aggregate/pluck/positional raw SQL to ORM
A deeper re-audit (with an adversarial skeptic) of the queries left raw
in the prior commit found more that have exact ORM equivalents:

- scalar SUM/MAX  -> frappe.qb + Sum/Max .run()[0][0]
- SUM ... GROUP BY -> frappe.qb .groupby().select(Sum().as_()) run(as_dict)
- name IN (values) -> get_all(filters={'f': ['in', ...]})
- sql_list(select col) -> get_all(pluck='col')
- bulk UPDATE ... = NULL/value -> frappe.db.set_value(filters, field, val)
- positional as_list reads -> get_all(..., as_list=True) (+ sorted())

Note: get_value(dt, filters, 'sum(x)') and get_all(fields=['sum(x)'])
are rejected by frappe ('SQL functions are not allowed as strings'), so
aggregates go through frappe.qb. get_all(as_list=True) returns a tuple
(not a list), so consumers that mutate use sorted().

All affected test modules pass on MariaDB.
2026-06-26 11:34:31 +05:30
Mihir Kandoi
0776f7f7fa test: convert trivially-equivalent raw SQL to ORM helpers
Convert test-only raw frappe.db.sql calls that have an exact ORM
equivalent: full-table/filtered deletes -> frappe.db.delete, count ->
frappe.db.count, row-existence assertions -> frappe.db.exists,
single-row scalar fetches -> frappe.db.get_value, and simple
equality/range-filter selects -> frappe.get_all. No behaviour change.

Raw SQL that genuinely needs it is left as-is (dynamic identifiers,
aggregates/group-by, positional as_list consumers, DB-catalog
introspection).
2026-06-26 10:44:17 +05:30
Mihir Kandoi
0e54e532ff refactor(accounts): use frappe.get_all for trial balance account fetch
The Account metadata fetch in the DuckDB trial-balance path is a plain
static SELECT (fixed columns, single company filter, order by lft).
Convert it to frappe.get_all. Verified on Postgres: identical 98 rows,
same order and same dict payload as the raw query.
2026-06-26 10:44:05 +05:30
Nabin Hait
de45ab7fc9 test: add coverage for Incorrect Stock Value Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:40:32 +05:30
Nabin Hait
5ad8ea3f17 test: add coverage for Item Where Used report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:39:59 +05:30
Nabin Hait
b537e8b183 test: add coverage for Delayed Order Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:20 +05:30
Nabin Hait
ce5239132c test: add coverage for Delayed Item Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:12 +05:30
Nabin Hait
8bef2b13a1 test: add coverage for Batch Item Expiry Status report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:05 +05:30
Nabin Hait
c69077cd3a test: add coverage for BOM Search report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:32:58 +05:30
Nabin Hait
4a6b189221 test: add coverage for Item Variant Details report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:55 +05:30
Nabin Hait
08ce18fe58 test: add coverage for Item Price Stock report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:40 +05:30
Nabin Hait
7661e5ed96 test: add coverage for Serial No Ledger report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:18 +05:30
Nabin Hait
dddaa80f99 test: add coverage for Serial and Batch Summary report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:11 +05:30
Nabin Hait
09be6fed9a test: add coverage for Stock Ledger Invariant Check report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:59 +05:30
Nabin Hait
5790bcf99d test: add coverage for Batch-Wise Balance History report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:04 +05:30
Mihir Kandoi
ea9bf932d8 Merge pull request #56510 from frappe/revert-56497-serial-batch-bundle-print-none-fix
Revert "fix: handle missing serial and batch bundle in print format"
2026-06-26 10:18:43 +05:30
Mihir Kandoi
993a011005 Revert "fix: handle missing serial and batch bundle in print format" 2026-06-26 10:07:23 +05:30
Nabin Hait
d18177665b test: add coverage for Trial Balance for Party report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:11:45 +05:30
Nabin Hait
507bc0930e Merge pull request #56412 from frappe/chore/fixed-asset-register-test-coverage
test: Fixed Asset Register report coverage
2026-06-25 22:13:39 +05:30
MochaMind
c3d2ebd734 fix: sync translations from crowdin (#56396) 2026-06-25 16:35:56 +02:00
Shllokkk
142d80d7de Merge pull request #56497 from Shllokkk/serial-batch-bundle-print-none-fix
fix: handle missing serial and batch bundle in print format
2026-06-25 19:22:23 +05:30
Mihir Kandoi
c8f86099e2 Merge pull request #56479 from mihir-kandoi/messages/crm
chore: rewrite user-facing messages in crm module
2026-06-25 18:47:25 +05:30
Mihir Kandoi
ab1e949752 Merge pull request #56482 from mihir-kandoi/messages/erpnext_integrations
chore: rewrite user-facing messages in erpnext_integrations module
2026-06-25 18:43:52 +05:30
Mihir Kandoi
8ad90338a3 Merge pull request #56469 from mihir-kandoi/messages/controllers
chore: rewrite user-facing messages in controllers module
2026-06-25 18:38:23 +05:30
Mihir Kandoi
d2ff0913df Merge pull request #56476 from mihir-kandoi/messages/projects
chore: rewrite user-facing messages in projects module
2026-06-25 18:35:00 +05:30
Mihir Kandoi
05dd44246f Merge pull request #56475 from mihir-kandoi/messages/subcontracting
chore: rewrite user-facing messages in subcontracting module
2026-06-25 18:33:55 +05:30
Mihir Kandoi
6e22f4b063 Merge pull request #56498 from mihir-kandoi/messages/exchange-rate-server-grammar
chore: fix grammar in Exchange Rate Revaluation validation message
2026-06-25 18:28:11 +05:30
Mihir Kandoi
d737c39131 Merge pull request #56473 from mihir-kandoi/messages/selling
chore: rewrite user-facing messages in selling module
2026-06-25 18:24:13 +05:30
Nabin Hait
38385432f6 test: assert group-by totals on delta to avoid shared-category leak
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:22:58 +05:30
Nabin Hait
78fd06048f style: apply ruff formatting
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:21:08 +05:30
Mihir Kandoi
1b68445313 Merge pull request #56480 from mihir-kandoi/messages/utilities
chore: rewrite user-facing messages in utilities module
2026-06-25 18:17:50 +05:30
Nabin Hait
3a3f56350f Merge remote-tracking branch 'origin/develop' into chore/fixed-asset-register-test-coverage
# Conflicts:
#	erpnext/assets/report/fixed_asset_register/test_fixed_asset_register.py
2026-06-25 18:10:53 +05:30
Mihir Kandoi
70bd57d3e7 chore: fix grammar in Exchange Rate Revaluation validation message
"to getting entries" -> "to get entries". Matches the client-side message
fixed in #56484 so the same missing Company/Posting Date validation reads
identically on client and server. Part of #53976.
2026-06-25 17:44:57 +05:30
Shllokkk
548d90df4f fix: handle missing serial and batch bundle in print format 2026-06-25 17:42:47 +05:30
Mihir Kandoi
9ec043ffcb chore: rewrite user-facing messages in Erpnext Integrations module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:33 +05:30
Mihir Kandoi
10744d1332 chore: rewrite user-facing messages in Utilities module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:24 +05:30
Mihir Kandoi
b161e5aa79 chore: rewrite user-facing messages in Crm module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:20 +05:30
Mihir Kandoi
4efb43d977 chore: rewrite user-facing messages in Projects module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:08 +05:30
Mihir Kandoi
f5bf915104 chore: rewrite user-facing messages in Subcontracting module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:03 +05:30
Mihir Kandoi
33562a6a86 chore: rewrite user-facing messages in Selling module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:36:59 +05:30
Mihir Kandoi
95b6cf2847 chore: rewrite user-facing messages in Controllers module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:36:50 +05:30
Mihir Kandoi
11da80c9c5 chore: rewrite user-facing messages in Stock module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:36:40 +05:30
Nabin Hait
0a462f8d2f test: cover combined depreciation and revaluation in FA register
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:46:02 +05:30
Nabin Hait
f4413ebda3 test: cover depreciation, sale, revaluation and capitalization in FA register
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:30:22 +05:30
Nabin Hait
168c24f8f0 test: add coverage for Fixed Asset Register report
The Fixed Asset Register report had no test file. Add tests for asset value
(net purchase amount, reduced by opening accumulated depreciation), the
status (In Location) and asset category filters, and group-by-asset-category
value totals.
2026-06-24 12:23:04 +05:30
Mohd Haris
adfef48a65 fix: allow rename for Quality Inspection Parameter
The Quality Inspection Parameter DocType did not have `allow_rename`
enabled, so the "Rename" action was hidden from the form's menu
(the 3-dots / ⋮ options). Since the DocType is auto-named from the
`parameter` field (`autoname: field:parameter`), users had no way to
correct or change a parameter's name once created.

Enable `allow_rename` so users can rename a Quality Inspection
Parameter from the form menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:29:43 +05:30
158 changed files with 178226 additions and 49460 deletions

View File

@@ -21,7 +21,7 @@ jobs:
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
uses: pre-commit/action@v3.0.1
semgrep:
name: semgrep

View File

@@ -48,7 +48,6 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class OverlapError(frappe.ValidationError):
@@ -36,8 +37,20 @@ class AccountingPeriod(Document):
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_overlap()
def validate_dates(self):
if getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("Start Date cannot be after End Date"))
if getdate(self.end_date) > getdate(nowdate()):
frappe.throw(
_(
"Accounting Period cannot be created for a future date. End Date {0} is after today."
).format(frappe.bold(frappe.format(self.end_date, "Date")))
)
def before_insert(self):
self.bootstrap_doctypes_for_closing()

View File

@@ -2,7 +2,7 @@
# See license.txt
import frappe
from frappe.utils import add_months, nowdate
from frappe.utils import nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
@@ -93,7 +93,7 @@ def create_accounting_period(**args):
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.end_date = args.end_date or nowdate()
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})

View File

@@ -73,7 +73,7 @@ class ExchangeRateRevaluation(Document):
def validate_mandatory(self):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
frappe.throw(_("Please select Company and Posting Date to get entries"))
def before_submit(self):
self.remove_accounts_without_gain_loss()

View File

@@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -63,13 +64,9 @@ class TestLoyaltyPointEntry(ERPNextTestSuite):
self.assertEqual(doc.loyalty_points, -7)
# Check balance
balance = frappe.db.sql(
"""
SELECT SUM(loyalty_points)
FROM `tabLoyalty Point Entry`
WHERE customer = %s
""",
(self.customer_name,),
)[0][0]
lpe = frappe.qb.DocType("Loyalty Point Entry")
balance = (
frappe.qb.from_(lpe).select(Sum(lpe.loyalty_points)).where(lpe.customer == self.customer_name)
).run()[0][0]
self.assertEqual(balance, 3) # 10 added, 7 redeemed

View File

@@ -3,6 +3,7 @@
import unittest
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, getdate, today
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
@@ -262,14 +263,12 @@ class TestLoyaltyProgram(ERPNextTestSuite):
def get_points_earned(self):
def get_returned_amount():
returned_amount = frappe.db.sql(
"""
select sum(grand_total)
from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
""",
self.name,
)
si = frappe.qb.DocType("Sales Invoice")
returned_amount = (
frappe.qb.from_(si)
.select(Sum(si.grand_total))
.where((si.docstatus == 1) & (si.is_return == 1) & (si.return_against == self.name))
).run()
return abs(flt(returned_amount[0][0])) if returned_amount else 0
lp_details = get_loyalty_program_details_with_points(

View File

@@ -10,9 +10,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
class TestPOSInvoiceMerging(POSInvoiceTestMixin):
def clear_pos_data(self):
frappe.db.sql("delete from `tabPOS Opening Entry`;")
frappe.db.sql("delete from `tabPOS Closing Entry`;")
frappe.db.sql("delete from `tabPOS Invoice`;")
frappe.db.delete("POS Opening Entry")
frappe.db.delete("POS Closing Entry")
frappe.db.delete("POS Invoice")
def setUp(self):
self.clear_pos_data()

View File

@@ -25,15 +25,11 @@ class TestPOSProfile(ERPNextTestSuite):
items = get_items_list(doc, doc.company)
customers = get_customers_list(doc)
products_count = frappe.db.sql(
""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1
)
customers_count = frappe.db.sql(
""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'"""
)
products_count = frappe.db.count("Item", {"item_group": "_Test Item Group"})
customers_count = frappe.db.count("Customer", {"customer_group": "_Test Customer Group"})
self.assertEqual(len(items), products_count[0][0])
self.assertEqual(len(customers), customers_count[0][0])
self.assertEqual(len(items), products_count)
self.assertEqual(len(customers), customers_count)
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
@@ -83,7 +79,6 @@ class TestPOSProfile(ERPNextTestSuite):
def get_customers_list(pos_profile=None):
if pos_profile is None:
pos_profile = {}
cond = "1=1"
customer_groups = []
if pos_profile.get("customer_groups"):
# Get customers based on the customer groups defined in the POS profile
@@ -91,14 +86,16 @@ def get_customers_list(pos_profile=None):
customer_groups.extend(
[d.get("name") for d in get_child_nodes("Customer Group", d.get("customer_group"))]
)
cond = "customer_group in ({})".format(", ".join(["%s"] * len(customer_groups)))
filters = {"disabled": 0}
if customer_groups:
filters["customer_group"] = ["in", customer_groups]
return (
frappe.db.sql(
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
and {cond}""",
tuple(customer_groups),
as_dict=1,
frappe.get_all(
"Customer",
filters=filters,
fields=["name", "customer_name", "customer_group", "territory"],
)
or {}
)
@@ -135,8 +132,8 @@ def get_items_list(pos_profile, company):
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Payment Method`")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.delete("POS Payment Method")
frappe.db.delete("POS Profile")
args = frappe._dict(args)

View File

@@ -91,7 +91,9 @@ class TestPricingRule(ERPNextTestSuite):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 5)
frappe.db.sql("update `tabPricing Rule` set priority=NULL where campaign='_Test Campaign'")
frappe.db.set_value(
"Pricing Rule", {"campaign": "_Test Campaign"}, "priority", None, update_modified=False
)
from erpnext.accounts.doctype.pricing_rule.utils import MultiplePricingRuleConflict
self.assertRaises(MultiplePricingRuleConflict, get_item_details, args)

View File

@@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form
import erpnext
@@ -130,6 +131,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
get_purchase_document_details,
)
from erpnext.stock.utils import get_valuation_method
doc = self.doc
tax_service = TaxService(doc)
@@ -329,20 +331,33 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
self.make_provisional_gl_entry(gl_entries, item)
if not doc.is_internal_transfer():
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": base_amount,
"debit_in_transaction_currency": amount,
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
handled = False
if (
item.item_code
and item.item_code in stock_items
and item.get("purchase_receipt")
and not doc.is_return
and get_valuation_method(item.item_code, doc.company) == "Standard Cost"
):
handled = self.make_standard_cost_srbnb_split(
gl_entries, item, expense_account, account_currency, base_amount
)
if not handled:
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": base_amount,
"debit_in_transaction_currency": amount,
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
)
# check if the exchange rate has changed
if (
@@ -515,6 +530,107 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
},
)
def make_standard_cost_srbnb_split(
self, gl_entries, item, expense_account, account_currency, base_amount
):
"""For a Standard Cost item billed against a Purchase Receipt, clear SRBNB at the standard
value the receipt actually booked and post the (Net Amount - standard) difference to the
Purchase Price Variance account. Returns False (caller falls back) if the receipt value
can't be resolved."""
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
get_purchase_price_variance_account,
)
doc = self.doc
precision = item.precision("base_net_amount")
standard_value = flt(self.get_pr_stock_value(item), precision)
if not standard_value:
return False
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": standard_value,
"debit_in_transaction_currency": flt(standard_value / doc.conversion_rate, precision),
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
variance = flt(base_amount - standard_value, precision)
if variance:
gl_entries.append(
self.get_gl_dict(
{
"account": get_purchase_price_variance_account(item.item_code, doc.company),
"against": doc.supplier,
"debit": variance,
"debit_in_transaction_currency": flt(variance / doc.conversion_rate, precision),
"remarks": doc.get("remarks") or _("Purchase Price Variance"),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
item=item,
)
)
return True
def get_pr_stock_value(self, item):
"""Stock value (at standard) the linked Purchase Receipt booked for the quantity this invoice
row is billing.
Accepted and rejected stock for the same receipt row share `voucher_detail_no`, so the
warehouse filter is required: without it the accepted warehouse's SRBNB would be cleared at
accepted + rejected value and post the wrong Purchase Price Variance amount. The accepted
warehouse is read from the receipt row itself (not the invoice row, which may be unset on a
non-stock invoice).
The receipt's full accepted value is pro-rated to the invoiced quantity, so a partial bill
clears SRBNB (and posts PPV) for only the units it covers, not the whole receipt row."""
pr_detail = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, ["warehouse", "stock_qty"], as_dict=True
)
if not pr_detail or not pr_detail.warehouse:
return 0.0
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Sum(sle.stock_value_difference))
.where(
(sle.voucher_type == "Purchase Receipt")
& (sle.voucher_no == item.purchase_receipt)
& (sle.voucher_detail_no == item.pr_detail)
& (sle.warehouse == pr_detail.warehouse)
& (sle.is_cancelled == 0)
)
).run()
accepted_value = flt(result[0][0]) if result and result[0][0] else 0.0
if not accepted_value or not flt(pr_detail.stock_qty):
return accepted_value
# Pro-rate to the quantity being billed by this invoice row (handles partial billing).
return accepted_value * flt(item.stock_qty) / flt(pr_detail.stock_qty)
def get_stock_variance_account(self, item):
"""For Standard Cost items the purchase-price-vs-standard difference is a Purchase Price
Variance; for all other items it keeps the existing behaviour (default expense account)."""
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
get_purchase_price_variance_account,
)
from erpnext.stock.utils import get_valuation_method
if item.item_code and get_valuation_method(item.item_code, self.doc.company) == "Standard Cost":
return get_purchase_price_variance_account(item.item_code, self.doc.company)
return self.doc.get_company_default("default_expense_account")
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
doc = self.doc
net_amt_precision = item.precision("base_net_amount")
@@ -536,7 +652,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
cost_of_goods_sold_account = self.get_stock_variance_account(item)
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
@@ -561,7 +677,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
and warehouse_debit_amount
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
):
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
cost_of_goods_sold_account = self.get_stock_variance_account(item)
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount

View File

@@ -135,38 +135,9 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
template = frappe.get_doc("Payment Terms Template", "_Test Payment Term Template")
first_term = frappe.get_doc("Payment Term", template.terms[0].payment_term)
expected_payment_term = first_term.description or first_term.name
filters = {
"company": self.company,
@@ -193,12 +164,10 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
self.assertEqual([pi.name, expected_payment_term], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
project = frappe.get_doc("Project", {"project_name": "_Test Project"})
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
@@ -227,9 +196,7 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
project = frappe.get_doc("Project", {"project_name": "_Test Project"})
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name

View File

@@ -1422,10 +1422,10 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
# must be applied explicitly. The report should only show permitted customers.
original_customer = self.customer
second_customer = "_Test AR Perm Customer"
second_customer = "_Test Customer 1"
# create_customer overrides self.customer, so build the restricted invoice first
self.create_customer(customer_name=second_customer)
self.customer = second_customer
self.create_sales_invoice(no_payment_schedule=True)
self.customer = original_customer

View File

@@ -59,16 +59,9 @@ class TestCashFlow(ERPNextTestSuite):
def test_cash_purchase_of_asset_is_investing_outflow(self):
"""Buying a fixed asset for cash is an investing outflow that reduces net change in cash."""
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
create_account(
account_name="_Test Cash Flow Asset",
company=self.company,
parent_account="Fixed Assets - _TC",
account_type="Fixed Asset",
)
asset_account = "_Test Cash Flow Asset - _TC"
asset_account = "Office Equipment - _TC"
before = self.net_change_in_cash()
# debit the fixed asset, credit cash -> cash goes out

View File

@@ -64,16 +64,12 @@ class TestGeneralLedger(ERPNextTestSuite):
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def test_opening_total_and_closing_balances(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
self.clear_old_entries()
account = create_account(
account_name="_Test GL Account", company=self.company, parent_account="Current Assets - _TC"
)
offset = create_account(
account_name="_Test GL Offset", company=self.company, parent_account="Current Assets - _TC"
)
# reuse bootstrap non-party accounts; clear_old_entries() leaves them clean of GL
account = "_Test Account Cost for Goods Sold - _TC"
offset = "_Test Bank - _TC"
make_journal_entry(account, offset, 1000, posting_date=add_days(today(), -60), submit=True) # opening
make_journal_entry(account, offset, 200, posting_date=today(), submit=True) # in period
@@ -87,19 +83,13 @@ class TestGeneralLedger(ERPNextTestSuite):
self.assertEqual(labelled["'Closing (Opening + Total)'"]["debit"], 1200)
def test_categorize_by_account_subtotals(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
self.clear_old_entries()
account_a = create_account(
account_name="_Test GL Account A", company=self.company, parent_account="Current Assets - _TC"
)
account_b = create_account(
account_name="_Test GL Account B", company=self.company, parent_account="Current Assets - _TC"
)
offset = create_account(
account_name="_Test GL Offset", company=self.company, parent_account="Current Assets - _TC"
)
# reuse bootstrap non-party accounts; clear_old_entries() leaves them clean of GL
account_a = "_Test Account Cost for Goods Sold - _TC"
account_b = "_Test Bank - _TC"
offset = "_Test Cash - _TC"
make_journal_entry(account_a, offset, 300, posting_date=today(), submit=True)
make_journal_entry(account_b, offset, 400, posting_date=today(), submit=True)

View File

@@ -597,11 +597,21 @@ def execute_synced_report(filters):
def get_data_duckdb(filters, conn):
# accounts and all metadata via frappe.db — only GL Entry comes from DuckDB
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
as_dict=True,
accounts = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[
"name",
"account_number",
"parent_account",
"account_name",
"root_type",
"report_type",
"is_group",
"lft",
"rgt",
],
order_by="lft",
)
if not accounts:
return None

View File

@@ -0,0 +1,117 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.trial_balance_for_party.trial_balance_for_party import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestTrialBalanceForParty(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"company": "_Test Company",
"party_type": "Customer",
"fiscal_year": "_Test Fiscal Year 2026",
"from_date": "2026-01-01",
"to_date": "2026-12-31",
**extra,
}
)
return execute(filters)[1]
def party_row(self, party, **extra):
return next(row for row in self.run_report(party=party, **extra) if row.get("party") == party)
def test_sales_invoice_shown_as_period_debit(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
row = self.party_row(customer)
self.assertEqual(row["opening_debit"], 0)
self.assertEqual(row["debit"], 10000)
self.assertEqual(row["credit"], 0)
self.assertEqual(row["closing_debit"], 10000)
self.assertEqual(row["closing_credit"], 0)
def test_receipt_nets_invoice_in_closing(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
create_payment_entry(
payment_type="Receive",
party_type="Customer",
party=customer,
paid_from="Debtors - _TC",
paid_to="_Test Bank - _TC",
paid_amount=4000,
save=True,
submit=True,
)
row = self.party_row(customer)
self.assertEqual(row["debit"], 10000)
self.assertEqual(row["credit"], 4000)
# closing nets debit against credit: 10000 - 4000
self.assertEqual(row["closing_debit"], 6000)
self.assertEqual(row["closing_credit"], 0)
def test_prior_period_invoice_shown_as_opening(self):
customer = "_Test Customer"
# invoice dated before from_date should land in the opening balance, not within-period
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2025-12-01")
row = self.party_row(customer)
self.assertEqual(row["opening_debit"], 10000)
self.assertEqual(row["debit"], 0)
self.assertEqual(row["closing_debit"], 10000)
def test_exclude_zero_balance_parties(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
create_payment_entry(
payment_type="Receive",
party_type="Customer",
party=customer,
paid_from="Debtors - _TC",
paid_to="_Test Bank - _TC",
paid_amount=10000,
save=True,
submit=True,
)
# fully settled party still shows by default ...
self.assertEqual(self.party_row(customer)["closing_debit"], 0)
# ... but is hidden when zero-balance parties are excluded
parties = {row.get("party") for row in self.run_report(exclude_zero_balance_parties=1)}
self.assertNotIn(customer, parties)
def test_purchase_invoice_shown_as_supplier_credit(self):
supplier = "_Test Supplier"
make_purchase_invoice(supplier=supplier, qty=1, rate=8000, posting_date="2026-06-01")
row = self.party_row(supplier, party_type="Supplier")
self.assertEqual(row["credit"], 8000)
self.assertEqual(row["debit"], 0)
self.assertEqual(row["closing_credit"], 8000)
self.assertEqual(row["closing_debit"], 0)
def test_totals_row_sums_party_rows(self):
create_sales_invoice(customer="_Test Customer 1", qty=1, rate=10000, posting_date="2026-06-01")
create_sales_invoice(customer="_Test Customer 2", qty=1, rate=6000, posting_date="2026-06-01")
data = self.run_report()
totals = data[-1] # totals row is appended last
party_rows = data[:-1]
for column in (
"opening_debit",
"opening_credit",
"debit",
"credit",
"closing_debit",
"closing_credit",
):
self.assertEqual(totals[column], sum(row[column] for row in party_rows))

View File

@@ -238,22 +238,13 @@ class TestAssetRepair(ERPNextTestSuite):
submit=1,
)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
.where((gle.voucher_type == "Asset Repair") & (gle.voucher_no == asset_repair.name))
.groupby(gle.account)
).run(as_dict=True)
self.assertTrue(gl_entries)
@@ -287,22 +278,13 @@ class TestAssetRepair(ERPNextTestSuite):
submit=1,
)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
.where((gle.voucher_type == "Asset Repair") & (gle.voucher_no == asset_repair.name))
.groupby(gle.account)
).run(as_dict=True)
self.assertTrue(gl_entries)

View File

@@ -94,11 +94,12 @@ class TestAssetValueAdjustment(ERPNextTestSuite):
("_Test Fixed Asset - _TC", 0.0, 4625.29),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""",
adj_doc.journal_entry,
gle = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Journal Entry", "voucher_no": adj_doc.journal_entry},
fields=["account", "debit", "credit"],
order_by="account",
as_list=True,
)
self.assertSequenceEqual(gle, expected_gle)
@@ -184,11 +185,12 @@ class TestAssetValueAdjustment(ERPNextTestSuite):
("_Test Fixed Asset - _TC", 0.0, 5175.29),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""",
adj_doc.journal_entry,
gle = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Journal Entry", "voucher_no": adj_doc.journal_entry},
fields=["account", "debit", "credit"],
order_by="account",
as_list=True,
)
self.assertSequenceEqual(gle, expected_gle)

View File

@@ -3,11 +3,26 @@
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import AssetSetup, create_asset
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
create_asset_capitalization,
)
from erpnext.assets.doctype.asset_value_adjustment.test_asset_value_adjustment import (
make_asset_value_adjustment,
)
from erpnext.assets.report.fixed_asset_register.fixed_asset_register import execute
class TestFixedAssetRegister(AssetSetup):
def run_report(self, **extra):
filters = frappe._dict(company="_Test Company", **extra)
return execute(filters)[1]
def report_row(self, asset_name, **extra):
return next(row for row in self.run_report(**extra) if row["asset_id"] == asset_name)
def test_report_lists_submitted_asset(self):
"""Exercises the report's converted queries -- including the depreciation aggregate that groups
by asset.name (must be valid on Postgres) -- by asserting a submitted asset is listed."""
@@ -18,16 +33,170 @@ class TestFixedAssetRegister(AssetSetup):
location="Test Location",
submit=1,
)
filters = frappe._dict(
{
"company": "_Test Company",
"status": "In Location",
"filter_based_on": "Date Range",
"from_date": "2020-01-01",
"to_date": "2030-12-31",
"date_based_on": "Purchase Date",
}
ids = {
row["asset_id"]
for row in self.run_report(
status="In Location",
filter_based_on="Date Range",
from_date="2020-01-01",
to_date="2030-12-31",
date_based_on="Purchase Date",
)
}
self.assertIn(asset.name, ids)
def test_asset_appears_with_purchase_value(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
data = execute(filters)[1]
asset_ids = {row.get("asset_id") for row in data}
self.assertIn(asset.name, asset_ids)
row = self.report_row(asset.name)
self.assertEqual(row["net_purchase_amount"], 100000)
self.assertEqual(row["asset_value"], 100000) # no depreciation yet
self.assertEqual(row["asset_category"], "Computers")
def test_asset_value_reduced_by_opening_depreciation(self):
asset = create_asset(
item_code="Macbook Pro",
net_purchase_amount=100000,
purchase_amount=100000,
opening_accumulated_depreciation=20000,
opening_number_of_booked_depreciations=2,
submit=True,
)
row = self.report_row(asset.name)
self.assertEqual(row["opening_accumulated_depreciation"], 20000)
self.assertEqual(row["asset_value"], 80000) # 100000 - 20000
def test_status_in_location_filter_shows_active_asset(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
ids = {row["asset_id"] for row in self.run_report(status="In Location")}
self.assertIn(asset.name, ids)
def test_asset_category_filter(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
ids = {row["asset_id"] for row in self.run_report(asset_category="Computers")}
self.assertIn(asset.name, ids)
def test_group_by_asset_category_sums_values(self):
before_net, before_value = self.computers_group_totals()
create_asset(item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True)
create_asset(
item_code="Macbook Pro",
asset_name="Macbook Pro 2",
net_purchase_amount=50000,
purchase_amount=50000,
submit=True,
)
after_net, after_value = self.computers_group_totals()
# assert on the delta so pre-existing Computers assets don't skew the totals
self.assertEqual(after_net - before_net, 150000)
self.assertEqual(after_value - before_value, 150000)
def computers_group_totals(self):
row = next(
(r for r in self.run_report(group_by="Asset Category") if r["asset_category"] == "Computers"),
None,
)
return (row["net_purchase_amount"], row["asset_value"]) if row else (0, 0)
def test_booked_depreciation_reduces_asset_value(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
available_for_use_date="2019-12-31",
depreciation_start_date="2020-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
expected_value_after_useful_life=10000,
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
# books one depreciation entry of (100000 - 10000) / 3 = 30000
post_depreciation_entries(date="2021-01-01")
row = self.report_row(asset.name)
self.assertEqual(row["depreciated_amount"], 30000)
self.assertEqual(row["asset_value"], 70000) # 100000 - 30000
def test_revaluation_adjusts_asset_value(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
# revalue the asset upwards by 20000
make_asset_value_adjustment(
asset=asset.name, current_asset_value=100000, new_asset_value=120000
).submit()
row = self.report_row(asset.name)
self.assertEqual(row["asset_value"], 120000) # 100000 + 20000 revaluation
def test_depreciation_and_revaluation_together(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
available_for_use_date="2019-12-31",
depreciation_start_date="2020-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
expected_value_after_useful_life=10000,
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
# books one depreciation entry of (100000 - 10000) / 3 = 30000, leaving 70000
post_depreciation_entries(date="2021-01-01")
# revalue the depreciated asset down from 70000 to 60000
make_asset_value_adjustment(
asset=asset.name, current_asset_value=70000, new_asset_value=60000
).submit()
row = self.report_row(asset.name)
self.assertEqual(row["depreciated_amount"], 30000)
self.assertEqual(row["asset_value"], 60000) # 100000 - 30000 depreciation - 10000 revaluation
def test_sold_asset_hidden_from_in_location_and_shown_in_disposed(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=80000)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
self.assertNotIn(asset.name, {row["asset_id"] for row in self.run_report(status="In Location")})
self.assertIn(asset.name, {row["asset_id"] for row in self.run_report(status="Disposed")})
def test_capitalized_asset_hidden_from_in_location_and_shown_in_disposed(self):
consumed_asset = create_asset(
asset_name="Consumed Asset",
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
composite_asset = create_asset(
asset_name="Composite Asset", asset_type="Composite Asset", submit=False
)
create_asset_capitalization(
target_asset=composite_asset.name, consumed_asset=consumed_asset.name, submit=1
)
self.assertEqual(frappe.db.get_value("Asset", consumed_asset.name, "status"), "Capitalized")
self.assertNotIn(
consumed_asset.name, {row["asset_id"] for row in self.run_report(status="In Location")}
)
self.assertIn(consumed_asset.name, {row["asset_id"] for row in self.run_report(status="Disposed")})

View File

@@ -547,6 +547,7 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
}
],
@@ -561,7 +562,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-22 12:23:09.241125",
"modified": "2026-06-27 16:12:33.190257",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -274,7 +274,7 @@ class AccountsController(TransactionBase):
if invalid_advances := [x for x in self.advances if not x.reference_type or not x.reference_name]:
frappe.throw(
_(
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
"Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
).format(
frappe.bold(comma_and([x.idx for x in invalid_advances])),
frappe.bold(_("Advance Payments")),
@@ -1233,7 +1233,7 @@ class AccountsController(TransactionBase):
{"sales_order": None, "sales_order_item": None},
)
frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po)))
frappe.msgprint(_("Purchase Orders {0} are unlinked").format("\n".join(linked_po)))
def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default

View File

@@ -129,7 +129,7 @@ class BuyingController(SubcontractingController):
msg += f"<li>{po} ({date})</li>"
msg += "</ul>"
frappe.throw(_(msg))
frappe.throw(msg)
def create_package_for_transfer(self) -> None:
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
@@ -287,7 +287,7 @@ class BuyingController(SubcontractingController):
if self.is_return and len(not_cancelled_asset):
frappe.throw(
_(
"{} has submitted assets linked to it. You need to cancel the assets to create purchase return."
"{0} has submitted assets linked to it. You need to cancel the assets to create purchase return."
).format(self.return_against),
title=_("Not Allowed"),
)
@@ -738,7 +738,7 @@ class BuyingController(SubcontractingController):
frappe.throw(
_("Row #{idx}: {field_label} can not be negative for item {item_code}.").format(
idx=item_row["idx"],
field_label=frappe.get_meta(item_row.doctype).get_label(fieldname),
field_label=_(frappe.get_meta(item_row.doctype).get_label(fieldname)),
item_code=frappe.bold(item_row["item_code"]),
)
)

View File

@@ -77,7 +77,7 @@ def validate_return_against(doc):
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(
_("'Update Stock' can not be checked because items are not delivered via {0}").format(
_("'Update Stock' cannot be checked because items are not delivered via {0}").format(
doc.return_against
)
)

View File

@@ -297,7 +297,7 @@ class SellingController(StockController):
throw(
_(
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be atleast {4}.<br><br>Alternatively,
Selling {3} should be at least {4}.<br><br>Alternatively,
you can disable '{5}' in {6} to bypass
this validation."""
).format(
@@ -869,7 +869,7 @@ class SellingController(StockController):
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
duplicate_items_msg += "<br><br>"
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
duplicate_items_msg += _("Please enable {0} in {1} to allow same item in multiple rows").format(
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
get_link_to_form("Selling Settings", "Selling Settings"),
)
@@ -898,7 +898,7 @@ class SellingController(StockController):
if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items):
msg = _("Target Warehouse is set for some items but the customer is not an internal customer.")
msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
msg += " " + _("This {0} will be treated as material transfer.").format(_(self.doctype))
frappe.msgprint(msg, title="Internal Transfer", alert=True)
def validate_items(self):

View File

@@ -286,10 +286,10 @@ class StatusUpdater(Document):
# get unique transactions to update
for d in self.get_all_children():
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
frappe.throw(_("For an item {0}, quantity must be a positive number").format(d.item_code))
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
frappe.throw(_("For an item {0}, quantity must be a negative number").format(d.item_code))
if (
not selling_negative_rate_allowed and self.doctype in ["Sales Invoice", "Delivery Note"]
@@ -300,7 +300,7 @@ class StatusUpdater(Document):
if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0:
frappe.throw(
_(
"For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}"
"For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}"
).format(
frappe.bold(d.item_code),
frappe.bold(_("`Allow Negative rates for Items`")),

View File

@@ -820,6 +820,8 @@ def create_item_wise_repost_entries(
):
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
from erpnext.stock.utils import get_valuation_method
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
distinct_item_warehouses = set()
@@ -831,6 +833,11 @@ def create_item_wise_repost_entries(
continue
distinct_item_warehouses.add(item_wh)
# Standard Cost items don't need a full repost: a backdated entry only shifts future balances
# (qty and value at the standard rate), which is done in place by update_qty_in_future_sle.
if get_valuation_method(sle.item_code) == "Standard Cost":
continue
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = "Item and Warehouse"

View File

@@ -211,7 +211,7 @@ class SubcontractingController(StockController):
)
if bom_item != item.item_code:
frappe.throw(
_("Row {0}: Please select an valid BOM for Item {1}.").format(
_("Row {0}: Please select a valid BOM for Item {1}.").format(
item.idx, item.item_name
)
)
@@ -1053,8 +1053,10 @@ class SubcontractingController(StockController):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
msg = _("The Batch No {0} has not been supplied against the {1} {2}").format(
frappe.bold(row.get("batch_no")), self.subcontract_data.order_doctype, link
)
frappe.throw(msg, title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_and_batch_bundle") and self.__transferred_items.get(key).get("serial_no"):
@@ -1066,8 +1068,10 @@ class SubcontractingController(StockController):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
msg = _("The Serial Nos {0} have not been supplied against the {1} {2}").format(
incorrect_sn, self.subcontract_data.order_doctype, link
)
frappe.throw(msg, title=_("Incorrect Serial Number Consumed"))
def __validate_supplied_or_received_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:

View File

@@ -78,7 +78,7 @@ class SubcontractingInwardController:
):
frappe.throw(
_(
"Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
"Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead."
).format(item.idx, get_link_to_form("Item", item.item_code))
)
@@ -126,7 +126,7 @@ class SubcontractingInwardController:
or frappe.get_cached_value("Subcontracting Inward Order Item", item.scio_detail, "item_code")
):
frappe.throw(
_("Row #{0}: Item {1} mismatch. Changing of item code is not permitted.").format(
_("Row #{0}: Item {1} mismatch. Changing the item code is not permitted.").format(
item.idx, get_link_to_form("Item", item.item_code)
)
)
@@ -441,7 +441,7 @@ class SubcontractingInwardController:
):
frappe.throw(
_(
"Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
"Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
).format(
item.idx,
", ".join([get_link_to_form("Batch No", bn) for bn in incorrect_batch_nos]),

View File

@@ -131,9 +131,9 @@ class calculate_taxes_and_totals:
if item.item_tax_template not in taxes:
item.item_tax_template = taxes[0]
frappe.msgprint(
_("Row {0}: Item Tax template updated as per validity and rate applied").format(
item.idx, frappe.bold(item.item_code)
)
_(
"Row {0}: Item Tax template for {1} updated as per validity and rate applied"
).format(item.idx, frappe.bold(item.item_code))
)
# For correct tax_amount calculation re-computation is required
@@ -564,7 +564,7 @@ class calculate_taxes_and_totals:
+ "<br>".join(invalid_rows)
)
frappe.throw(_(message))
frappe.throw(message)
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total

View File

@@ -56,10 +56,10 @@ def validate_filters(filters):
frappe.throw(_("{0} is mandatory").format(_(f)))
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
frappe.throw(_("Fiscal Year {0} does not exist").format(filters.get("fiscal_year")))
if filters.get("based_on") == filters.get("group_by"):
frappe.throw(_("'Based On' and 'Group By' can not be same"))
frappe.throw(_("'Based On' and 'Group By' can not be the same"))
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
frappe.throw(

View File

@@ -308,4 +308,4 @@ def add_role_for_portal_user(portal_user, role):
return
user_doc.add_roles(role)
frappe.msgprint(_("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True)
frappe.msgprint(_("Added {1} role to user {0}.").format(frappe.bold(user_doc.name), role), alert=True)

View File

@@ -59,7 +59,7 @@ class AppointmentBookingSettings(Document):
err_msg = _("<b>From Time</b> cannot be later than <b>To Time</b> for {0}").format(
record.day_of_week
)
frappe.throw(_(err_msg))
frappe.throw(err_msg)
def duration_is_divisible(self, from_time, to_time):
timedelta = to_time - from_time

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
@@ -49,7 +49,7 @@ class CRMSettings(Document):
if self.enable_frappe_crm_data_synchronization and not self.allowed_users:
frappe.throw(
_(
"Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
"Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
)
)

View File

@@ -171,7 +171,7 @@ def add_bank_accounts(response: str | dict, bank: str | dict, company: str):
except Exception:
frappe.log_error("Plaid Link Error")
frappe.throw(
_("There was an error updating Bank Account {} while linking with Plaid.").format(
_("There was an error updating Bank Account {0} while linking with Plaid.").format(
existing_bank_account
),
title=_("Plaid Link Failed"),

File diff suppressed because it is too large Load Diff

62914
erpnext/locale/bg.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

62954
erpnext/locale/uz.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,10 +97,10 @@ class TestBOM(ERPNextTestSuite):
update_cost_in_all_boms_in_test()
# check if new valuation rate updated in all BOMs
for d in frappe.db.sql(
"""select base_rate from `tabBOM Item`
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
as_dict=1,
for d in frappe.get_all(
"BOM Item",
filters={"item_code": "_Test Item 2", "docstatus": 1, "parenttype": "BOM"},
fields=["base_rate"],
):
self.assertEqual(d.base_rate, rm_base_rate + 10)
@@ -881,12 +881,8 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
warehouse_list = [warehouse_list]
if not warehouse_list:
warehouse_list = frappe.db.sql_list(
"""
select warehouse from `tabBin`
where item_code=%s and actual_qty > 0
""",
item_code,
warehouse_list = frappe.get_all(
"Bin", filters={"item_code": item_code, "actual_qty": [">", 0]}, pluck="warehouse"
)
if not warehouse_list:

View File

@@ -5249,11 +5249,8 @@ def update_job_card(job_card, jc_qty=None, days=None):
def get_secondary_item_details(bom_no):
secondary_items = {}
for item in frappe.db.sql(
"""select item_code, stock_qty from `tabBOM Secondary Item`
where parent = %s""",
bom_no,
as_dict=1,
for item in frappe.get_all(
"BOM Secondary Item", filters={"parent": bom_no}, fields=["item_code", "stock_qty"]
):
secondary_items[item.item_code] = item.stock_qty

View File

@@ -49,7 +49,7 @@ class TestProject(ERPNextTestSuite):
def test_project_with_template_having_no_parent_and_depend_tasks(self):
project_name = "Test Project with Template - No Parent and Dependend Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.db.delete("Task", {"project": project_name})
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task with No Parent and Dependency")
@@ -82,7 +82,7 @@ class TestProject(ERPNextTestSuite):
if frappe.db.get_value("Project", {"project_name": project_name}, "name"):
project_name = frappe.db.get_value("Project", {"project_name": project_name}, "name")
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.db.delete("Task", {"project": project_name})
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task Parent")
@@ -137,7 +137,7 @@ class TestProject(ERPNextTestSuite):
def test_project_template_having_dependent_tasks(self):
project_name = "Test Project with Template - Dependent Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.db.delete("Task", {"project": project_name})
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task for Dependency")
@@ -252,7 +252,7 @@ class TestProject(ERPNextTestSuite):
def test_project_having_no_tasks_complete(self):
project_name = "Test Project - No Tasks Completion"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.db.delete("Task", {"project": project_name})
frappe.delete_doc("Project", project_name)
project = frappe.get_doc(

View File

@@ -144,7 +144,7 @@ class Task(NestedSet):
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw(
_(
"Cannot complete task {0} as its dependant task {1} are not completed / cancelled."
"Cannot complete task {0} as its dependent task {1} is not completed / cancelled."
).format(frappe.bold(self.name), frappe.bold(d.task))
)
@@ -316,7 +316,7 @@ class Task(NestedSet):
def on_trash(self):
if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You can not delete this Task."))
throw(_("Child Task exists for this Task. You cannot delete this Task."))
self.update_nsm_model()

View File

@@ -105,7 +105,7 @@ class TimesheetDetail(Document):
def validate_dates(self):
"""Validate that to_time is not before from_time."""
if self.from_time and self.to_time and time_diff_in_hours(self.to_time, self.from_time) < 0:
frappe.throw(_("To Time cannot be before from date"))
frappe.throw(_("To Time cannot be before From Time"))
def validate_parent_project(self, parent_project: str):
"""Validate that project is same as Timesheet's parent project."""

View File

@@ -23,15 +23,12 @@ erpnext.accounts.taxes = {
onload: function (frm) {
if (frm.get_field("taxes")) {
frm.set_query("account_head", "taxes", function (doc) {
let account_type = ["Tax", "Chargeable"];
if (frm.cscript.tax_table == "Sales Taxes and Charges") {
var account_type = ["Tax", "Chargeable", "Expense Account"];
account_type.push("Expense Account");
} else {
var account_type = [
"Tax",
"Chargeable",
"Income Account",
"Expenses Included In Valuation",
];
account_type.push("Income Account", "Expenses Included In Valuation");
}
return {

View File

@@ -952,14 +952,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt(
total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
var total_amount_to_pay = flt(
total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,
@@ -1004,14 +1005,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
async set_total_amount_to_default_mop() {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt(
total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
var total_amount_to_pay = flt(
total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,

View File

@@ -1291,13 +1291,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var set_party_account = function (set_pricing) {
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (me.frm.doc.doctype == "Sales Invoice") {
var party_type = "Customer";
var party_account_field = "debit_to";
} else {
var party_type = "Supplier";
var party_account_field = "credit_to";
}
let party_type = me.frm.doc.doctype == "Sales Invoice" ? "Customer" : "Supplier";
let party_account_field = me.frm.doc.doctype == "Sales Invoice" ? "debit_to" : "credit_to";
var party = me.frm.doc[frappe.model.scrub(party_type)];
if (
@@ -2071,7 +2066,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
var item_grid = this.frm.fields_dict["operations"].grid;
let item_grid = this.frm.fields_dict["operations"].grid;
$.each(["base_operating_cost", "base_hour_rate"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -2079,7 +2074,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
var item_grid = this.frm.fields_dict["secondary_items"].grid;
let item_grid = this.frm.fields_dict["secondary_items"].grid;
$.each(["base_rate", "base_amount"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -2470,7 +2465,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
row_to_modify[key] = pr_row[key];
}
if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) {
if (Object.prototype.hasOwnProperty.call(this.frm.doc, "is_pos") && this.frm.doc.is_pos) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center");
if (r.message.cost_center) {
row_to_modify["cost_center"] = r.message.cost_center;
@@ -2735,7 +2730,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
$.each(me.frm.doc.items || [], function (i, item) {
if (
item.name &&
r.message.hasOwnProperty(item.name) &&
Object.prototype.hasOwnProperty.call(r.message, item.name) &&
r.message[item.name].item_tax_template
) {
item.item_tax_template = r.message[item.name].item_tax_template;

View File

@@ -681,6 +681,7 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
}
],
@@ -695,7 +696,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-22 12:23:19.196991",
"modified": "2026-06-27 16:12:10.457900",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -158,7 +158,7 @@ class Customer(TransactionBase):
new_customer_name = f"{self.customer_name} - {cstr(count)}"
msgprint(
_("Changed customer name to '{}' as '{}' already exists.").format(
_("Changed customer name to '{0}' as '{1}' already exists.").format(
new_customer_name, self.customer_name
),
title=_("Note"),
@@ -356,7 +356,7 @@ class Customer(TransactionBase):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(
_(
"A Customer Group exists with same name please change the Customer name or rename the Customer Group"
"A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group"
),
frappe.NameError,
)
@@ -406,7 +406,7 @@ class Customer(TransactionBase):
if flt(limit.credit_limit) < outstanding_amt:
frappe.throw(
_(
"""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}"""
"""New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}"""
).format(outstanding_amt)
)
@@ -440,7 +440,7 @@ class Customer(TransactionBase):
self.loyalty_program = loyalty_program[0]
else:
frappe.msgprint(
_("Multiple Loyalty Programs found for Customer {}. Please select manually.").format(
_("Multiple Loyalty Programs found for Customer {0}. Please select manually.").format(
frappe.bold(self.customer_name)
)
)

View File

@@ -172,7 +172,7 @@ def make_address(args, is_primary_address=1, is_shipping_address=1):
if reqd_fields:
msg = _("Following fields are mandatory to create address:")
frappe.throw(
"{} <br><br> <ul>{}</ul>".format(msg, "\n".join(reqd_fields)),
msg + " <br><br> <ul>{}</ul>".format("\n".join(reqd_fields)),
title=_("Missing Values Required"),
)

View File

@@ -32,4 +32,6 @@ class PartySpecificItem(Document):
},
)
if exists:
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))
frappe.throw(
_("This item filter has already been applied for the {0}").format(_(self.party_type))
)

View File

@@ -118,9 +118,9 @@ class ProductBundle(Document):
if len(invoice_links):
frappe.throw(
"This Product Bundle is linked with {}. You will have to cancel these documents in order to delete this Product Bundle".format(
", ".join(invoice_links)
),
_(
"This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
).format(", ".join(invoice_links)),
title=_("Not Allowed"),
)

View File

@@ -7,6 +7,7 @@ from unittest.mock import patch
import frappe
import frappe.permissions
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.query_builder.functions import Sum
from frappe.tests import change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
@@ -907,10 +908,12 @@ class TestSalesOrder(ERPNextTestSuite):
item_doc.save()
else:
# update valid from
frappe.db.sql(
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template},
frappe.db.set_value(
"Item Tax",
{"parent": item, "item_tax_template": tax_template},
"valid_from",
today(),
update_modified=False,
)
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
@@ -960,10 +963,12 @@ class TestSalesOrder(ERPNextTestSuite):
self.assertEqual(so.taxes[1].total, 480)
# teardown
frappe.db.sql(
"""UPDATE `tabItem Tax` set valid_from = NULL
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template},
frappe.db.set_value(
"Item Tax",
{"parent": item, "item_tax_template": tax_template},
"valid_from",
None,
update_modified=False,
)
so.cancel()
so.delete()
@@ -1559,10 +1564,12 @@ class TestSalesOrder(ERPNextTestSuite):
# Check if Work Orders were raised
for item in so_item_name:
wo_qty = frappe.db.sql(
"select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s",
(so.name, item),
)
wo = frappe.qb.DocType("Work Order")
wo_qty = (
frappe.qb.from_(wo)
.select(Sum(wo.qty))
.where((wo.sales_order == so.name) & (wo.sales_order_item == item))
).run()
self.assertEqual(wo_qty[0][0], so_item_name.get(item))
def test_advance_payment_entry_unlink_against_sales_order(self):
@@ -1742,9 +1749,7 @@ class TestSalesOrder(ERPNextTestSuite):
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1
make_raw_material_request(mr_dict, so.company, so.name)
mr = frappe.db.sql(
"""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1
)[0]
mr = frappe.get_all("Material Request", fields=["name"], order_by="creation desc", limit=1)[0]
mr_doc = frappe.get_doc("Material Request", mr.get("name"))
self.assertEqual(mr_doc.items[0].sales_order, so.name)

View File

@@ -16,7 +16,7 @@ def validate_filters(from_date, to_date, company):
frappe.throw(_("To Date must be greater than From Date"))
if not company:
frappe.throw(_("Please Select a Company"))
frappe.throw(_("Please select a Company"))
@frappe.whitelist()

View File

@@ -47,7 +47,7 @@ class SalesPartnerSummaryReport:
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
frappe.throw(_("DocType can be one of {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))

View File

@@ -19,7 +19,7 @@ class SalesPartnerSummaryReportTestMixin(ERPNextTestSuite):
with self.assertRaisesRegex(
frappe.ValidationError,
_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
_("DocType can be one of {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
):
run(self.report_name, self.filters)

View File

@@ -129,6 +129,7 @@
"valuation_method",
"column_break_32",
"stock_adjustment_account",
"default_purchase_price_variance_account",
"stock_received_but_not_billed",
"stock_delivered_but_not_billed",
"disable_sdbnb_in_sr",
@@ -491,6 +492,15 @@
"no_copy": 1,
"options": "Account"
},
{
"description": "Used for items valued at Standard Cost: the difference between the purchase price and the standard rate is booked here.",
"fieldname": "default_purchase_price_variance_account",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Purchase Price Variance Account",
"no_copy": 1,
"options": "Account"
},
{
"fieldname": "column_break_32",
"fieldtype": "Column Break"
@@ -1004,7 +1014,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2026-05-14 16:50:34.132345",
"modified": "2026-06-26 10:05:00.000000",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.query_builder.functions import Max
from frappe.utils.nestedset import (
NestedSetChildExistsError,
NestedSetInvalidMergeError,
@@ -20,7 +21,8 @@ class TestItemGroup(ERPNextTestSuite):
def test_basic_tree(self, records=None):
min_lft = 1
max_rgt = frappe.db.sql("select max(rgt) from `tabItem Group`")[0][0]
ig = frappe.qb.DocType("Item Group")
max_rgt = frappe.qb.from_(ig).select(Max(ig.rgt)).run()[0][0]
if not records:
records = self.globalTestRecords["Item Group"][2:]
@@ -131,12 +133,7 @@ class TestItemGroup(ERPNextTestSuite):
frappe.db.get_value("Item Group", parent_item_group, "rgt")
ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3")
ancestors = frappe.db.sql(
"""select name, rgt from `tabItem Group`
where name in ({})""".format(", ".join(["%s"] * len(ancestors))),
tuple(ancestors),
as_dict=True,
)
ancestors = frappe.get_all("Item Group", filters={"name": ["in", ancestors]}, fields=["name", "rgt"])
frappe.delete_doc("Item Group", "_Test Item Group B - 3")
records_to_test = self.globalTestRecords["Item Group"][2:]
@@ -168,9 +165,8 @@ class TestItemGroup(ERPNextTestSuite):
self.test_basic_tree()
# move its children back
for name in frappe.db.sql_list(
"""select name from `tabItem Group`
where parent_item_group='_Test Item Group C'"""
for name in frappe.get_all(
"Item Group", filters={"parent_item_group": "_Test Item Group C"}, pluck="name"
):
doc = frappe.get_doc("Item Group", name)
doc.parent_item_group = "_Test Item Group B"
@@ -218,11 +214,7 @@ class TestItemGroup(ERPNextTestSuite):
def get_no_of_children(item_groups, no_of_children):
children = []
for ig in item_groups:
children += frappe.db.sql_list(
"""select name from `tabItem Group`
where ifnull(parent_item_group, '')=%s""",
ig or "",
)
children += frappe.get_all("Item Group", filters={"parent_item_group": ig}, pluck="name")
if len(children):
return get_no_of_children(children, no_of_children + len(children))

View File

@@ -390,7 +390,7 @@ def validate_serial_no_with_batch(serial_nos, item_code):
serial_no_link = ",".join(get_link_to_form("Serial No", sn) for sn in serial_nos)
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
message = _("Serial Nos") if len(serial_nos) > 1 else _("Serial No")
frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link))

View File

@@ -277,19 +277,27 @@ def update_qty(bin_name, args):
- flt(bin_details.reserved_qty_for_production_plan)
)
frappe.db.set_value(
"Bin",
bin_name,
{
"actual_qty": actual_qty,
"ordered_qty": ordered_qty,
"reserved_qty": reserved_qty,
"indented_qty": indented_qty,
"planned_qty": planned_qty,
"projected_qty": projected_qty,
},
update_modified=True,
)
bin_values = {
"actual_qty": actual_qty,
"ordered_qty": ordered_qty,
"reserved_qty": reserved_qty,
"indented_qty": indented_qty,
"planned_qty": planned_qty,
"projected_qty": projected_qty,
}
# Standard Cost items are not reposted on backdated entries, so the Bin's stock value is not
# refreshed by a repost. Keep it in step with the balance at the standard rate.
from erpnext.stock.utils import get_valuation_method
if get_valuation_method(args.get("item_code")) == "Standard Cost":
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import get_item_standard_rate
bin_values["stock_value"] = flt(actual_qty) * flt(
get_item_standard_rate(args.get("item_code"), args.get("company"))
)
frappe.db.set_value("Bin", bin_name, bin_values, update_modified=True)
def get_actual_qty(item_code, warehouse):

View File

@@ -79,7 +79,7 @@ def make_sales_invoice(
target.run_method("set_po_nos")
if len(target.get("items")) == 0:
frappe.throw(_("All these items have already been Invoiced/Returned"))
frappe.throw(_("All these items have already been invoiced/returned"))
if args and args.get("merge_taxes"):
merge_taxes(source, target)

View File

@@ -216,7 +216,7 @@ class DeliveryTrip(Document):
(list of list of str): List of address routes split at locks, if optimize is `True`
"""
if not self.driver_address:
frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing."))
frappe.throw(_("Cannot calculate arrival time as the driver address is missing."))
home_address = get_address_display(frappe.get_doc("Address", self.driver_address).as_dict())

View File

@@ -384,7 +384,7 @@
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Valuation Method",
"options": "\nFIFO\nMoving Average\nLIFO"
"options": "\nFIFO\nMoving Average\nLIFO\nStandard Cost"
},
{
"depends_on": "is_stock_item",
@@ -1090,7 +1090,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2026-05-27 10:18:46.862670",
"modified": "2026-06-26 10:05:00.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -143,7 +143,7 @@ class Item(Document):
taxes: DF.Table[ItemTax]
total_projected_qty: DF.Float
uoms: DF.Table[UOMConversionDetail]
valuation_method: DF.Literal["", "FIFO", "Moving Average", "LIFO"]
valuation_method: DF.Literal["", "FIFO", "Moving Average", "LIFO", "Standard Cost"]
valuation_rate: DF.Currency
variant_based_on: DF.Literal["Item Attribute", "Manufacturer"]
variant_of: DF.Link | None
@@ -239,6 +239,7 @@ class Item(Document):
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.validate_standard_cost_change()
self.validate_item_tax_net_rate_range()
if not self.is_new():
@@ -463,7 +464,7 @@ class Item(Document):
def validate_item_type(self):
if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset:
frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item"))
frappe.throw(_("'Has Serial No' cannot be 'Yes' for non-stock item"))
if self.has_serial_no == 0 and self.serial_no_series:
self.serial_no_series = None
@@ -1060,6 +1061,30 @@ class Item(Document):
for d in self.attributes:
d.variant_of = self.variant_of
def validate_standard_cost_change(self):
"""Once stock exists, an item's valuation method cannot be switched to or from Standard
Cost — either change would leave existing stock valued on a basis the ledger never
recorded."""
if not self.is_standard_cost_valuation_change():
return
if self.stock_ledger_created():
frappe.throw(
_(
"Valuation Method cannot be changed to or from 'Standard Cost' for {0} because stock transactions already exist for it."
).format(frappe.bold(self.name))
)
def is_standard_cost_valuation_change(self):
"""True if this save switches the valuation method into or out of Standard Cost."""
if self.is_new() or not self.has_value_changed("valuation_method"):
return False
previous = self.get_doc_before_save()
was_standard = previous and previous.valuation_method == "Standard Cost"
is_standard = self.valuation_method == "Standard Cost"
return bool(was_standard or is_standard)
def cant_change(self):
if self.is_new():
return
@@ -1508,7 +1533,9 @@ def validate_item_default_company_links(item_defaults: list[ItemDefault]) -> Non
company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True)
if company and company != item_default.company:
frappe.throw(
_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format(
_(
"Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}."
).format(
item_default.idx,
doctype,
frappe.bold(item_default.get(field)),

View File

@@ -33,7 +33,7 @@ class ItemAlternative(Document):
def has_alternative_item(self):
if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"):
frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code))
frappe.throw(_("Cannot set alternative item for the item {0}").format(self.item_code))
def validate_alternative_item(self):
if self.item_code == self.alternative_item_code:
@@ -65,7 +65,7 @@ class ItemAlternative(Document):
indicator="Orange",
)
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}")
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {0}")
if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
@@ -81,7 +81,7 @@ class ItemAlternative(Document):
"name": ("!=", self.name),
},
):
frappe.throw(_("Already record exists for the item {0}").format(self.item_code))
frappe.throw(_("Record already exists for the item {0}").format(self.item_code))
@frappe.whitelist()

View File

@@ -34,6 +34,7 @@
"default_provisional_account",
"purchase_expense_account",
"purchase_expense_contra_account",
"purchase_price_variance_account",
"selling_defaults",
"column_break_sales",
"vf_selling_cost_center",
@@ -189,6 +190,14 @@
"options": "Account",
"show_description_on_click": 1
},
{
"description": "For Standard Cost items: the purchase price vs standard rate difference is booked here. Falls back to the Company's Default Purchase Price Variance Account.",
"fieldname": "purchase_price_variance_account",
"fieldtype": "Link",
"label": "Purchase Price Variance Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_purchase",
"fieldtype": "Column Break"
@@ -356,7 +365,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-06-03 17:25:35.982082",
"modified": "2026-06-26 10:05:00.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",

View File

@@ -68,7 +68,7 @@ class ItemPrice(Document):
if not price_list_details:
link = frappe.utils.get_link_to_form("Price List", self.price_list)
frappe.throw(f"The price list {link} does not exist or is disabled")
frappe.throw(_("The price list {0} does not exist or is disabled").format(link))
self.buying, self.selling, self.currency = price_list_details

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Item Standard Cost", {
setup(frm) {
// Only allow items whose effective valuation method is "Standard Cost".
frm.set_query("item_code", () => {
return {
query: "erpnext.stock.doctype.item_standard_cost.item_standard_cost.get_standard_cost_items",
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -0,0 +1,138 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2026-06-26 11:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"item_code",
"company",
"column_break_main",
"standard_rate",
"effective_date",
"revaluation_section",
"revaluation_entry",
"amended_from"
],
"fields": [
{
"default": "ISC-.YYYY.-",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "ISC-.YYYY.-",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_main",
"fieldtype": "Column Break"
},
{
"fieldname": "standard_rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Standard Valuation Rate",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"default": "Today",
"fieldname": "effective_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Effective Date",
"reqd": 1
},
{
"fieldname": "revaluation_section",
"fieldtype": "Section Break",
"label": "Revaluation"
},
{
"description": "Stock Reconciliation auto-created to revalue on-hand stock to the new standard rate.",
"fieldname": "revaluation_entry",
"fieldtype": "Link",
"label": "Revaluation Entry",
"no_copy": 1,
"options": "Stock Reconciliation",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Item Standard Cost",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-06-26 11:00:00.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Standard Cost",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,299 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Max
from frappe.utils import flt, get_datetime, get_link_to_form, getdate, nowtime, today
from frappe.utils.caching import request_cache
from erpnext.stock.utils import get_valuation_method
class ItemStandardCost(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amended_from: DF.Link | None
company: DF.Link
effective_date: DF.Date
item_code: DF.Link
naming_series: DF.Literal["ISC-.YYYY.-"]
revaluation_entry: DF.Link | None
standard_rate: DF.Currency
# end: auto-generated types
def validate(self):
self.validate_item()
self.validate_effective_date()
self.validate_rate()
def validate_item(self):
if not frappe.get_cached_value("Item", self.item_code, "is_stock_item"):
frappe.throw(_("{0} is not a stock item.").format(frappe.bold(self.item_code)))
if get_valuation_method(self.item_code, self.company) != "Standard Cost":
frappe.throw(
_("Valuation Method of Item {0} must be set to 'Standard Cost'.").format(
get_link_to_form("Item", self.item_code)
)
)
def validate_effective_date(self):
# Standard cost is set "as of now"; future-dating would leave a gap where new receipts
# are valued at a rate that is not yet effective.
if getdate(self.effective_date) > getdate(today()):
frappe.throw(_("Effective Date cannot be a future date."))
# Effective dates must be strictly increasing so the rate history can be read by date.
last = self.get_last_standard_cost()
if last and getdate(self.effective_date) <= getdate(last.effective_date):
frappe.throw(
_("Effective Date must be after {0} (the last Standard Cost {1}).").format(
frappe.bold(frappe.format(last.effective_date, "Date")),
get_link_to_form("Item Standard Cost", last.name),
)
)
def validate_rate(self):
if flt(self.standard_rate) <= 0:
frappe.throw(_("Standard Valuation Rate must be greater than zero."))
if self.get_last_standard_cost() is None:
# First-ever rate for this item+company: only allowed when no stock movement exists,
# so the item starts its life under Standard Cost (no historical revaluation needed).
if self.has_any_sle():
frappe.throw(
_(
"Standard Cost can only be set up for {0} in {1} before any stock transaction exists."
).format(get_link_to_form("Item", self.item_code), frappe.bold(self.company))
)
return
# R1: a rate change must be effective on/after the latest stock activity, so the
# revaluation entry it creates never sits behind existing transactions.
last_sle_date = self.get_last_sle_date()
if last_sle_date and getdate(self.effective_date) < getdate(last_sle_date):
frappe.throw(
_("Effective Date cannot be before the last stock transaction date {0}.").format(
frappe.bold(frappe.format(last_sle_date, "Date"))
)
)
def on_submit(self):
# This record is now the effective rate. Drop any request-cached lookup that may have read the
# previous (or missing) rate earlier in the request, so the revaluation below — and anything
# else in this request — reads the newly submitted rate.
clear_item_standard_rate_cache()
self.create_revaluation_entry()
def before_cancel(self):
frappe.throw(
_("Item Standard Cost cannot be cancelled. Submit a new record to change the standard rate.")
)
def create_revaluation_entry(self):
"""Revalue on-hand stock to the new standard rate via a Stock Reconciliation.
Submitted atomically: if the reconciliation cannot be submitted (closed period, frozen
accounts, etc.) the exception propagates and this submission is rolled back."""
balances = self.get_warehouse_wise_balance()
if not balances:
return
reco = frappe.new_doc("Stock Reconciliation")
reco.company = self.company
reco.purpose = "Stock Reconciliation"
reco.posting_date = self.effective_date
reco.posting_time = self.get_revaluation_posting_time()
reco.set_posting_time = 1
for row in balances:
reco.append(
"items",
{
"item_code": self.item_code,
"warehouse": row.warehouse,
"qty": row.actual_qty,
"valuation_rate": self.standard_rate,
},
)
reco.flags.via_item_standard_cost = True
reco.insert()
reco.submit()
self.db_set("revaluation_entry", reco.name)
def get_revaluation_posting_time(self):
"""Post the revaluation after the day's last stock movement.
The reconciliation asserts the current on-hand quantity (Bin.actual_qty). If it were posted
before later same-day movements, it would backdate that quantity ahead of them and corrupt the
qty/value timeline. Using the time of the last SLE on the effective date (the reconciliation
sorts after it on creation) keeps the snapshot at the correct point; if there is no movement
that day, the current time is safe since no later movement can exist."""
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Max(sle.posting_datetime))
.where(
(sle.item_code == self.item_code)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle.posting_date == getdate(self.effective_date))
)
).run()
last_datetime = result[0][0] if result and result[0][0] else None
# Keep microsecond precision: posting_datetime is compared at microsecond granularity, so a
# truncated time would sort the reco before a same-second movement. Matching the exact time
# lets the later creation order the reco after it.
return get_datetime(last_datetime).strftime("%H:%M:%S.%f") if last_datetime else nowtime()
def get_warehouse_wise_balance(self):
bin_table = frappe.qb.DocType("Bin")
warehouse = frappe.qb.DocType("Warehouse")
return (
frappe.qb.from_(bin_table)
.inner_join(warehouse)
.on(bin_table.warehouse == warehouse.name)
.select(bin_table.warehouse, bin_table.actual_qty)
.where(
(bin_table.item_code == self.item_code)
& (warehouse.company == self.company)
& (bin_table.actual_qty != 0)
)
).run(as_dict=True)
def get_last_standard_cost(self):
records = frappe.get_all(
"Item Standard Cost",
filters={
"item_code": self.item_code,
"company": self.company,
"docstatus": 1,
"name": ("!=", self.name),
},
fields=["name", "effective_date"],
order_by="effective_date desc, creation desc",
limit=1,
)
return records[0] if records else None
def get_last_sle_date(self):
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Max(sle.posting_date))
.where(
(sle.item_code == self.item_code) & (sle.company == self.company) & (sle.is_cancelled == 0)
)
).run()
return result[0][0] if result and result[0][0] else None
def has_any_sle(self):
return bool(
frappe.db.exists(
"Stock Ledger Entry",
{"item_code": self.item_code, "company": self.company, "is_cancelled": 0},
)
)
@request_cache
def get_item_standard_rate(item_code, company, posting_date=None):
"""Return the standard valuation rate effective for `item_code` in `company` as of
`posting_date` (defaults to today) — i.e. the latest submitted Item Standard Cost whose
effective date is on or before the posting date."""
posting_date = posting_date or today()
rate = frappe.get_all(
"Item Standard Cost",
filters={
"item_code": item_code,
"company": company,
"docstatus": 1,
"effective_date": ("<=", getdate(posting_date)),
},
fields=["standard_rate"],
order_by="effective_date desc, creation desc",
limit=1,
pluck="standard_rate",
)
return flt(rate[0]) if rate else None
def clear_item_standard_rate_cache():
"""Drop the request-cached results of `get_item_standard_rate` so reads after a new Item Standard
Cost is submitted see the fresh rate instead of a value cached earlier in the same request."""
cache = getattr(frappe.local, "request_cache", None)
if cache:
cache.pop(get_item_standard_rate.__wrapped__, None)
def get_purchase_price_variance_account(item_code, company):
"""Resolve the Purchase Price Variance account for a Standard Cost item: the per-company
Item Default override if set, otherwise the Company default."""
account = frappe.db.get_value(
"Item Default",
{"parent": item_code, "company": company},
"purchase_price_variance_account",
)
if not account:
account = frappe.get_cached_value("Company", company, "default_purchase_price_variance_account")
if not account:
frappe.throw(
_(
"Please set a Purchase Price Variance Account for Item {0} or a Default Purchase Price Variance Account in Company {1}."
).format(get_link_to_form("Item", item_code), frappe.bold(company))
)
return account
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_standard_cost_items(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None
):
"""Link-field query for Item Standard Cost: only items whose effective valuation method is
'Standard Cost' — i.e. the item is explicitly Standard Cost, or it has no valuation method of its
own and the applicable default (Company, else Stock Settings) is Standard Cost. This mirrors
get_valuation_method, so every shown item also passes validate_item."""
company = (filters or {}).get("company")
if company:
default_method = frappe.get_cached_value("Company", company, "valuation_method")
else:
default_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
if default_method == "Standard Cost":
# Items with no method of their own inherit the Standard Cost default.
valuation_condition = "and ifnull(item.valuation_method, '') in ('', 'Standard Cost')"
else:
valuation_condition = "and item.valuation_method = 'Standard Cost'"
return frappe.db.sql( # nosemgrep
f"""
select item.name, item.item_name
from `tabItem` item
where item.is_stock_item = 1
and item.disabled = 0
and item.has_variants = 0
{valuation_condition}
and ({searchfield} like %(txt)s or item.item_name like %(txt)s)
order by
(case when item.name like %(txt)s then 0 else 1 end),
item.name
limit %(page_len)s offset %(start)s
""",
{"txt": f"%{txt}%", "start": start, "page_len": page_len},
)

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