Compare commits

...

209 Commits

Author SHA1 Message Date
coderabbitai[bot]
16f13c75de 📝 Add docstrings to fix-mt940-statement-number-parsing
Docstrings generation was requested by @srujan00123.

* https://github.com/frappe/erpnext/pull/49682#issuecomment-3322321445

The following files were modified:

* `erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py`
* `erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py`
2025-09-23 03:58:23 +00:00
MochaMind
609164fb9a fix: sync translations from crowdin (#49660) 2025-09-22 16:21:27 +02:00
ruthra kumar
993ba4cf45 Merge pull request #49643 from aerele/po-super-class
fix(purchase order): invoke superclass onload method
2025-09-22 13:40:55 +05:30
ruthra kumar
bb081e46d7 Merge pull request #49644 from aerele/po-party-currency
fix(purchase order): get party type based on supplier field
2025-09-22 13:35:10 +05:30
ruthra kumar
6985f0efc3 Merge pull request #49653 from aerele/ar-exclude-employee
fix(accounts receivable): exclude employee transactions
2025-09-22 13:34:53 +05:30
Ravibharathi
302ff49b7f Merge pull request #49470 from aerele/bank-clearance-tax-calculation
fix(bank clearance): use base total taxes and charges if exists
2025-09-22 11:26:56 +05:30
Diptanil Saha
ee2b65806b Merge pull request #49649 from diptanilsaha/issue-replied
fix: set first_response_time on set_first_response
2025-09-22 00:05:07 +05:30
diptanilsaha
ba459204b0 fix: set first_response_time on status change of issue 2025-09-21 23:44:33 +05:30
MochaMind
7d4785784b fix: sync translations from crowdin (#49625) 2025-09-21 14:01:24 +02:00
MochaMind
35b503932d chore: update POT file (#49655) 2025-09-21 13:59:21 +02:00
venkat102
736a776d3d fix(accounts receivable): exclude employee transactions 2025-09-21 11:14:06 +05:30
venkat102
11b9b1adc5 fix(purchase order): get party type based on supplier field 2025-09-20 13:16:28 +05:30
venkat102
9f6bc7fe49 fix(purchase order): invoke superclass onload method 2025-09-20 13:07:57 +05:30
Mihir Kandoi
7e63f1d220 Merge pull request #49615 from aerele/fixed-support-48949
fix(stock): NoneType object error on stock entry
2025-09-18 20:16:40 +05:30
MochaMind
3d8502f408 fix: sync translations from crowdin (#49607) 2025-09-18 15:44:21 +02:00
ruthra kumar
c3e869c701 Merge pull request #49467 from aerele/qb-user-perm
fix: add condition for name
2025-09-18 16:41:16 +05:30
Kavin
aee03417de fix(stock): NoneType object error on stock entry 2025-09-18 16:31:07 +05:30
Diptanil Saha
14d8b87c8e Merge pull request #49608 from diptanilsaha/psoa_ar_show_future_payments
fix(process statement of accounts): total row in accounts receivable with future payments
2025-09-18 15:31:16 +05:30
rohitwaghchaure
2a86a1fb98 Merge pull request #49609 from rohitwaghchaure/fixed-drop-indexes-from-sle
fix: drop index batch_no_item_code_warehouse_index
2025-09-18 13:26:19 +05:30
Rohit Waghchaure
28180ccaa4 fix: drop index batch_no_item_code_warehouse_index 2025-09-18 13:08:26 +05:30
diptanilsaha
7bf17372b0 fix(process statement of accounts): total row in accounts receivable with future payments 2025-09-18 11:30:53 +05:30
venkat102
a5b881ea74 test: add test to validate user permission in qb 2025-09-18 00:02:10 +05:30
rohitwaghchaure
ac40b46a6d Merge pull request #49589 from rohitwaghchaure/fixed-op-cost-for-sfg
fix: operation cost for Semi FG item
2025-09-17 22:18:51 +05:30
Rohit Waghchaure
69682cb064 fix: operation cost for Semi FG item 2025-09-17 21:57:34 +05:30
Raheel Khan
8b543e5503 fix: skip receivable/payable account validation in payroll entry if party is not available (#49585)
* fix: skip receivable/payable account validation if party is not available in creation of payroll entry

* refactor: rename flag
2025-09-17 13:06:00 +00:00
Diógenes Souza
c4f90c3b34 fix: 'NoneType' object has no attribute 'get' in get_item_details.py (#49381) 2025-09-17 18:05:49 +05:30
Anwar Patel
244dce5098 fix: Incorrect filters in Voucher Child Table of Land Cost Voucher DocType (#49500)
* fix: company filter in receipt_document in landed cost voucher

* refactor: use strict equality
2025-09-17 17:43:29 +05:30
Bhavansathru
194ab87fef fix(payment-reconciliation): apply field precision for allocated and difference amounts (#49448)
fix(payment-reconciliation): handle allocated and difference amount with field precision for accurate exchange rate calculations
2025-09-17 17:04:02 +05:30
Logesh Periyasamy
e0299e1cbd fix: add option for currency field (#49572) 2025-09-17 17:01:53 +05:30
MochaMind
9c970acbda fix: sync translations from crowdin (#49581)
* fix: Norwegian Bokmal translations

* fix: French translations
2025-09-17 17:01:18 +05:30
rohitwaghchaure
06dde659c2 Merge pull request #49587 from rohitwaghchaure/fixed-support-48584
fix: incorrect current qty calculation for the batch
2025-09-17 17:00:23 +05:30
Logesh Periyasamy
9fc17e0e3a fix: project accounting dimension and gl posting date column width (#49563)
* fix: add width for posting_date

* fix: add project field for missing doctype

* fix: remove allow_on_submit to align with cost center
2025-09-17 16:57:48 +05:30
Rohit Waghchaure
535f8657ed fix: incorrect current qty calculation for the batch 2025-09-17 16:33:26 +05:30
Diptanil Saha
c77781a14f Merge pull request #49584 from diptanilsaha/custom-print-format-psoa
feat(accounts): enable print format selection in Process Statement of Accounts
2025-09-17 12:19:04 +05:30
diptanilsaha
c9d22386ed fix: added print format validation on process statement of accounts 2025-09-17 11:42:36 +05:30
diptanilsaha
2e7c3207c4 feat(accounts): enable print format selection in Process Statement of Accounts 2025-09-17 11:23:44 +05:30
Diptanil Saha
0aef591f5d Merge pull request #49281 from diptanilsaha/reporting_currency
feat: adding reporting_currency and dr/cr in reporting currency fields in GL Entry and Account Closing Balance
2025-09-17 10:44:04 +05:30
rohitwaghchaure
39049948b8 Merge pull request #49558 from rohitwaghchaure/fixed-valuation-rate-issue
fix: set basic rate on selection of the batch
2025-09-16 12:55:04 +05:30
ruthra kumar
39b9d798d9 Merge pull request #49511 from aerele/sales-invoice-tax-id
fix(sales invoice): fetch tax id from customer
2025-09-16 12:01:33 +05:30
Rohit Waghchaure
bebb8ae1ea fix: set basic rate on selection of the batch 2025-09-16 11:57:43 +05:30
ruthra kumar
1c8266af39 Merge pull request #49557 from frappe/l10n_develop
fix: sync translations from crowdin
2025-09-16 10:17:44 +05:30
MochaMind
73729f6ab0 fix: French translations 2025-09-16 07:06:51 +05:30
MochaMind
a287201011 fix: Norwegian Bokmal translations 2025-09-16 07:06:45 +05:30
MochaMind
a4e2fbdcf9 fix: Persian translations 2025-09-16 07:06:34 +05:30
MochaMind
21dc0a0b1a fix: Swedish translations 2025-09-16 07:06:18 +05:30
MochaMind
98626aaa6c fix: Portuguese translations 2025-09-16 07:06:09 +05:30
MochaMind
86a8015cea fix: German translations 2025-09-16 07:05:56 +05:30
MochaMind
8330b349d2 fix: Danish translations 2025-09-16 07:05:53 +05:30
Diptanil Saha
29197af11a Merge pull request #49438 from pipech/fix-thai-fiscal-year
fix(acc): thai fiscal year
2025-09-16 00:50:30 +05:30
rohitwaghchaure
a00a3868ed Merge pull request #49549 from rohitwaghchaure/fixed-not-allow-backdated-entries
fix: do not allow backdated entries if stock reco exists in future for serial or batch
2025-09-15 15:40:42 +05:30
Rohit Waghchaure
335dcc976c fix: do not allow backdated entries if stock reco exists in future for serial or batch 2025-09-15 15:22:41 +05:30
Raffael Meyer
6972f161b8 fix(accounts): dynamic zero cutoff (#48899)
* fix(accounts): dynamic zero cutoff

The cutoff for displaying zero values in accounting reports has so far been hardcoded to 0.005, giving wrong results for currencies that require a higher precision. This PR changes this to a dynamic value calculated from the Currency's _Fraction Units_.

* style: fix typo
2025-09-15 08:33:03 +02:00
Khushi Rawat
79b8505972 Merge pull request #49524 from KerollesFathy/fix-assets-movement-typos
fix: Assets Movement Typos
2025-09-15 11:54:56 +05:30
MochaMind
fe7b797e5f fix: sync translations from crowdin (#49550)
* fix: Spanish translations

* fix: Arabic translations

* fix: Czech translations

* fix: Danish translations

* fix: German translations

* fix: Hungarian translations

* fix: Italian translations

* fix: Dutch translations

* fix: Polish translations

* fix: Portuguese translations

* fix: Russian translations

* fix: Serbian (Cyrillic) translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Vietnamese translations

* fix: Portuguese, Brazilian translations

* fix: Indonesian translations

* fix: Persian translations

* fix: Thai translations

* fix: Croatian translations

* fix: Bosnian translations

* fix: Norwegian Bokmal translations

* fix: Serbian (Latin) translations

* fix: French translations

* fix: Esperanto translations
2025-09-15 07:45:05 +02:00
ravibharathi656
df329964dd fix(sales invoice): fetch tax id from customer 2025-09-15 10:12:20 +05:30
MochaMind
1845d12951 Merge pull request #49532 from frappe/l10n_develop 2025-09-14 20:33:38 +02:00
Raffael Meyer
67c2ab4c9f Merge pull request #49546 from frappe/pot_develop_2025-09-14
chore: update POT file
2025-09-14 13:40:25 +02:00
frappe-pr-bot
405e1ab6d3 chore: update POT file 2025-09-14 09:35:06 +00:00
rohitwaghchaure
cf066edd7f Merge pull request #49542 from rohitwaghchaure/fixed-validation-place
fix: validation for document status
2025-09-14 12:26:43 +05:30
Rohit Waghchaure
96e2e356b6 fix: validation for document status 2025-09-14 11:55:08 +05:30
rohitwaghchaure
0fef2d4b02 Merge pull request #49539 from rohitwaghchaure/fixed-support-48168
fix: precision issue for valuation rate calculation
2025-09-14 11:51:03 +05:30
rohitwaghchaure
b265b82f0b Merge pull request #49538 from rohitwaghchaure/fixed-docstatus-issue-of-sabb
fix: SABB document status validation
2025-09-14 11:25:33 +05:30
Rohit Waghchaure
c92a06d77d fix: precision issue for valuation rate calculation 2025-09-14 11:22:14 +05:30
Rohit Waghchaure
c0236191aa fix: SABB document status validation 2025-09-14 10:53:38 +05:30
KerollesFathy
5f083d55b5 fix: correct grammatical errors in asset movement validation messages 2025-09-10 18:00:44 +00:00
KerollesFathy
56da3bd2e4 fix: correct typo in asset movement purpose validation 2025-09-10 17:57:30 +00:00
Raffael Meyer
f27077a45f Merge pull request #49505 from frappe/l10n_develop
fix: sync translations from crowdin
2025-09-10 17:08:15 +02:00
MochaMind
38b51df17e fix: Norwegian Bokmal translations 2025-09-10 06:21:56 +05:30
rohitwaghchaure
f145e6267b Merge pull request #49503 from rohitwaghchaure/feat-allow-to-transfer-additonal-materials
Feat allow to transfer additional materials
2025-09-09 14:13:24 +05:30
Sagar Vora
09d9c0ddd3 Merge pull request #49506 from sagarvora/extend-not-override
fix: use the new `extend_doctype_class` hook
2025-09-09 06:21:19 +00:00
Rohit Waghchaure
e4b5507446 test: test case for additional material transfer 2025-09-09 11:44:17 +05:30
Sagar Vora
a028d856bc fix: use the new extend_doctype_class hook 2025-09-09 11:34:49 +05:30
MochaMind
e3e4680ad2 fix: Norwegian Bokmal translations 2025-09-09 06:13:49 +05:30
MochaMind
276406bc1c fix: Danish translations 2025-09-09 06:13:46 +05:30
MochaMind
156e46ccb0 fix: Esperanto translations 2025-09-09 06:13:42 +05:30
MochaMind
07a5aba2aa fix: Dutch translations 2025-09-09 06:13:38 +05:30
MochaMind
4ce8d9af6a fix: Serbian (Latin) translations 2025-09-09 06:13:35 +05:30
MochaMind
0474b8595b fix: Bosnian translations 2025-09-09 06:13:32 +05:30
MochaMind
ca8677a0ff fix: Croatian translations 2025-09-09 06:13:28 +05:30
MochaMind
03e5467ba2 fix: Thai translations 2025-09-09 06:13:25 +05:30
MochaMind
e960f8217b fix: Persian translations 2025-09-09 06:13:21 +05:30
MochaMind
a81e807a70 fix: Indonesian translations 2025-09-09 06:13:17 +05:30
MochaMind
3b4ee30dd7 fix: Portuguese, Brazilian translations 2025-09-09 06:13:14 +05:30
MochaMind
521ebc25aa fix: Vietnamese translations 2025-09-09 06:13:11 +05:30
MochaMind
1291df9a63 fix: Chinese Simplified translations 2025-09-09 06:13:08 +05:30
MochaMind
70d7ceb2f2 fix: Turkish translations 2025-09-09 06:13:04 +05:30
MochaMind
512419eee7 fix: Swedish translations 2025-09-09 06:13:01 +05:30
MochaMind
c6be380e83 fix: Serbian (Cyrillic) translations 2025-09-09 06:12:57 +05:30
MochaMind
200496254b fix: Russian translations 2025-09-09 06:12:53 +05:30
MochaMind
2596ef202b fix: Portuguese translations 2025-09-09 06:12:50 +05:30
MochaMind
ac71969512 fix: Polish translations 2025-09-09 06:12:46 +05:30
MochaMind
734a7b8be9 fix: Italian translations 2025-09-09 06:12:43 +05:30
MochaMind
aa9c4555fd fix: Hungarian translations 2025-09-09 06:12:39 +05:30
MochaMind
7e5be50997 fix: German translations 2025-09-09 06:12:36 +05:30
MochaMind
03e3a693ff fix: Czech translations 2025-09-09 06:12:33 +05:30
MochaMind
d4ae2f89b2 fix: Arabic translations 2025-09-09 06:12:30 +05:30
MochaMind
17c24a4168 fix: Spanish translations 2025-09-09 06:12:26 +05:30
MochaMind
c41824c4d0 fix: French translations 2025-09-09 06:12:23 +05:30
Rohit Waghchaure
3ad611966e feat: allow to transfer additional materials 2025-09-08 20:54:43 +05:30
Raffael Meyer
ddc97df31a Merge pull request #49355 from frappe/l10n_develop 2025-09-08 06:59:23 +02:00
Raffael Meyer
c21ebafaa5 Merge pull request #49494 from frappe/pot_develop_2025-09-07 2025-09-08 06:58:57 +02:00
MochaMind
17276a2c0c fix: Norwegian Bokmal translations 2025-09-08 05:47:32 +05:30
MochaMind
4ecdd1fd0e fix: Danish translations 2025-09-08 05:47:29 +05:30
MochaMind
7e093d08a9 fix: Swedish translations 2025-09-08 05:46:59 +05:30
rohitwaghchaure
955e8714ee Merge pull request #49477 from rohitwaghchaure/fixed-batch-qty-calculation
fix: batch qty calculation performance issue
2025-09-07 16:49:07 +05:30
Rohit Waghchaure
1a262483a4 fix: batch qty calculation performance issue 2025-09-07 16:24:00 +05:30
frappe-pr-bot
eead27560c chore: update POT file 2025-09-07 09:35:18 +00:00
MochaMind
b658330881 fix: Norwegian Bokmal translations 2025-09-07 05:45:53 +05:30
MochaMind
4f968f5c65 fix: Danish translations 2025-09-07 05:45:50 +05:30
MochaMind
838183941a fix: Swedish translations 2025-09-07 05:45:46 +05:30
Diptanil Saha
92d86eb30b Merge pull request #49485 from diptanilsaha/skip_bank_ac_creation
fix: skip 'Bank Account' creation on setup
2025-09-06 13:13:24 +05:30
diptanilsaha
efeda90cad fix: renamed temporary bank account to 'Demo Bank Account' 2025-09-06 02:38:53 +05:30
diptanilsaha
47d4319f83 fix: skip 'Bank Account' creation on setup 2025-09-06 02:03:03 +05:30
MochaMind
eb5a9db749 fix: Norwegian Bokmal translations 2025-09-05 05:50:49 +05:30
MochaMind
a2ccb5fa87 fix: Hungarian translations 2025-09-05 05:50:45 +05:30
MochaMind
b56dbe98cf fix: German translations 2025-09-05 05:50:42 +05:30
venkat102
cf5a2d6351 fix: add condition for name 2025-09-04 21:00:52 +05:30
rohitwaghchaure
564de01463 Merge pull request #49453 from rohitwaghchaure/fixed-fifo-valuation-for-non-batchwise-valuation
fix: non batch-wise valuation for batch item
2025-09-04 14:30:17 +05:30
rohitwaghchaure
ac8637d5a0 chore: fix test case 2025-09-04 13:00:32 +05:30
MochaMind
7c10775bc8 fix: Norwegian Bokmal translations 2025-09-04 05:09:55 +05:30
MochaMind
49a96f4306 fix: Serbian (Latin) translations 2025-09-04 05:09:52 +05:30
MochaMind
161ed5290e fix: Serbian (Cyrillic) translations 2025-09-04 05:09:48 +05:30
MochaMind
a0d3b931f3 fix: Hungarian translations 2025-09-04 05:09:44 +05:30
Rohit Waghchaure
11b82ba008 fix: non batch-wise valuation for batch item 2025-09-04 00:48:45 +05:30
rohitwaghchaure
1b73170e8c Merge pull request #49449 from rohitwaghchaure/fixed-support-46728
fix: incorrect stock value in the report
2025-09-04 00:47:16 +05:30
rohitwaghchaure
59c46a1789 Merge pull request #49425 from rohitwaghchaure/fixed-valuation-for-batch
fix: valuation for batch items
2025-09-04 00:47:02 +05:30
Rohit Waghchaure
24e0e3505d fix: valuation for batch items 2025-09-03 19:20:56 +05:30
Rohit Waghchaure
5824b5effd fix: incorrect stock value in the report 2025-09-03 19:03:08 +05:30
rohitwaghchaure
fba16efc07 Merge pull request #49442 from rohitwaghchaure/fixed-incorrect-batch-qty-issue
fix: incorrect batch qty
2025-09-03 16:42:09 +05:30
Mihir Kandoi
9aeb21d0c8 Merge pull request #49441 from mihir-kandoi/fix-wrong-wh-sre-transfer
fix: incorrect warehouse passed during SRE transfer in WO
2025-09-03 16:14:38 +05:30
Rohit Waghchaure
000135a3d4 fix: incorrect batch qty 2025-09-03 16:04:13 +05:30
Mihir Kandoi
260574719e fix: incorect warehouse passed during SRE transfer in WO 2025-09-03 15:53:53 +05:30
Mihir Kandoi
ead1ce2742 Merge pull request #49437 from mihir-kandoi/refactor-49320
refactor: PR 49320
2025-09-03 14:20:29 +05:30
Mihir Kandoi
991413608b refactor: PR 49320 2025-09-03 13:07:08 +05:30
pipech
fe74e0888b fix: thai fiscal year 2025-09-03 14:27:02 +07:00
diptanilsaha
ceff8c92fd fix: used wrong parameter for get_value to fetch previous fiscal year 2025-09-03 12:36:44 +05:30
diptanilsaha
d8babf66ae feat: add patch to set reporting_currency on GL Entry and Account Closing Balance 2025-09-03 12:36:44 +05:30
diptanilsaha
8dbbcf5ffb feat: dr/cr amounts in reporting_currency on account_closing_balance 2025-09-03 12:36:44 +05:30
diptanilsaha
fab9c4d7df feat: dr/cr amounts in reporting_currency on gl entries 2025-09-03 12:36:44 +05:30
diptanilsaha
2383051b74 feat: reporting_currency on company 2025-09-03 12:36:39 +05:30
MochaMind
d2d3294f02 fix: Norwegian Bokmal translations 2025-09-03 05:05:41 +05:30
MochaMind
62037301ee fix: Serbian (Latin) translations 2025-09-03 05:05:38 +05:30
MochaMind
b860f3d31d fix: Serbian (Cyrillic) translations 2025-09-03 05:05:34 +05:30
MochaMind
b8c72c05bb fix: Hungarian translations 2025-09-03 05:05:28 +05:30
Raffael Meyer
f1eda7c4ec Merge pull request #49374 from MarcCon/fix/ignore-permissions 2025-09-02 19:19:27 +02:00
Raffael Meyer
83eafe118e Merge pull request #49432 from barredterra/use-valid-ibans 2025-09-02 19:15:39 +02:00
barredterra
0b178b9449 test: use valid IBANs in party matching test case 2025-09-02 17:41:29 +02:00
ruthra kumar
1e7f374d6e Merge pull request #49306 from aerele/exchange-rate-revaluation-check-gain-loss
fix(exchange rate revaluation): add check for gain_loss
2025-09-02 13:33:55 +05:30
ruthra kumar
fcb86023cb Merge pull request #49332 from aerele/ticket-47028
perf: check PCV (smaller) table before checking GL Entries
2025-09-02 13:16:05 +05:30
Diptanil Saha
2dba591d37 Merge pull request #49407 from diptanilsaha/force_index_fetch_gle
fix(perf): applying consistent index to fetch gl entries for financial statements
2025-09-02 12:11:48 +05:30
ruthra kumar
385a2beaf9 Merge pull request #49379 from aerele/balance-type
fix: add is_cancelled in condition
2025-09-02 10:56:32 +05:30
MochaMind
cc2cc812cc fix: Norwegian Bokmal translations 2025-09-02 05:07:26 +05:30
MochaMind
a1c3c60fca fix: Danish translations 2025-09-02 05:07:23 +05:30
MochaMind
bbbd693c1c fix: Serbian (Latin) translations 2025-09-02 05:07:19 +05:30
MochaMind
f42f59a6b2 fix: Serbian (Cyrillic) translations 2025-09-02 05:07:16 +05:30
MochaMind
886cec797c fix: Hungarian translations 2025-09-02 05:07:10 +05:30
Raffael Meyer
2ea2c5c11e Merge pull request #49377 from barredterra/iban-options 2025-09-01 15:16:13 +02:00
ruthra kumar
b0f59ebf79 Merge pull request #49366 from aerele/tb-simple
fix(trial-balance-simple): ignore cancelled gl and add company filter
2025-09-01 16:45:38 +05:30
rohitwaghchaure
35fee187b6 Merge pull request #49403 from rohitwaghchaure/fixed-support-47626
fix: validation for Recreate Stock Ledgers
2025-09-01 16:12:10 +05:30
diptanilsaha
3e2fb85ae6 fix(perf): applying consistent index to fetch gl entries for financial statements 2025-09-01 15:15:42 +05:30
ruthra kumar
0cb5b571b0 Merge pull request #49335 from aerele/ticket-47331
fix: show company currency symbol
2025-09-01 15:06:10 +05:30
ruthra kumar
3131cf335e Merge pull request #49331 from aerele/patch-sync-auto-reconcile-config
fix: run sync auto reconcile config with force
2025-09-01 14:41:56 +05:30
Rohit Waghchaure
785845a425 fix: validation for Recreate Stock Ledgers 2025-09-01 13:39:35 +05:30
ruthra kumar
fa3ee91414 Merge pull request #49302 from aerele/fix/budget-warning
fix(budget):  fiscal year assignment and approver role in budget
2025-09-01 12:34:07 +05:30
MochaMind
f8fa8bdda7 fix: Norwegian Bokmal translations 2025-09-01 04:54:02 +05:30
MochaMind
69d509a098 fix: Danish translations 2025-09-01 04:53:59 +05:30
MochaMind
5a9fb3db1f fix: Esperanto translations 2025-09-01 04:53:56 +05:30
MochaMind
8c8bfd4277 fix: Dutch translations 2025-09-01 04:53:53 +05:30
MochaMind
47f0507643 fix: Serbian (Latin) translations 2025-09-01 04:53:50 +05:30
MochaMind
cb68c784fe fix: Bosnian translations 2025-09-01 04:53:47 +05:30
MochaMind
b1d3d39a11 fix: Croatian translations 2025-09-01 04:53:43 +05:30
MochaMind
31d9fc5367 fix: Thai translations 2025-09-01 04:53:40 +05:30
MochaMind
80a38732f9 fix: Persian translations 2025-09-01 04:53:37 +05:30
MochaMind
c228d1a05a fix: Indonesian translations 2025-09-01 04:53:33 +05:30
MochaMind
6bbba727a5 fix: Portuguese, Brazilian translations 2025-09-01 04:53:30 +05:30
MochaMind
0149bc633c fix: Vietnamese translations 2025-09-01 04:53:28 +05:30
MochaMind
574198bceb fix: Chinese Simplified translations 2025-09-01 04:53:24 +05:30
MochaMind
edd3383f7d fix: Turkish translations 2025-09-01 04:53:21 +05:30
MochaMind
2384b37305 fix: Swedish translations 2025-09-01 04:53:18 +05:30
MochaMind
c71dd00cc3 fix: Serbian (Cyrillic) translations 2025-09-01 04:53:14 +05:30
MochaMind
bd18ce7326 fix: Russian translations 2025-09-01 04:53:11 +05:30
MochaMind
9a85e1a811 fix: Portuguese translations 2025-09-01 04:53:08 +05:30
MochaMind
ad1b77f280 fix: Polish translations 2025-09-01 04:53:05 +05:30
MochaMind
99d89b207e fix: Italian translations 2025-09-01 04:53:02 +05:30
MochaMind
66f60c64bd fix: Hungarian translations 2025-09-01 04:52:58 +05:30
MochaMind
9f8abd585a fix: German translations 2025-09-01 04:52:55 +05:30
MochaMind
1f937a7c76 fix: Czech translations 2025-09-01 04:52:52 +05:30
MochaMind
9234e27a70 fix: Arabic translations 2025-09-01 04:52:49 +05:30
MochaMind
6ba476a3cd fix: Spanish translations 2025-09-01 04:52:46 +05:30
MochaMind
5f4c1f331d fix: French translations 2025-09-01 04:52:43 +05:30
MochaMind
0ad348d714 fix: Bosnian translations 2025-08-31 04:28:37 +05:30
MochaMind
7f1240e2eb fix: Croatian translations 2025-08-31 04:28:34 +05:30
MochaMind
6806c5e977 fix: Swedish translations 2025-08-31 04:28:31 +05:30
barredterra
4dd428de41 chore(Bank Account): remove IBAN validation
This is now handled by the Frappe Framework.
2025-08-29 15:23:13 +02:00
l0gesh29
77a9cf6398 fix: add is_cancelled in condition 2025-08-29 16:30:29 +05:30
barredterra
c45ea53889 fix(Bank Account): length of IBAN field 2025-08-29 12:32:29 +02:00
barredterra
cab262c147 feat: set options for IBAN fields
Sets options to "IBAN" for the respective Data fields to take advantage of a new formatting feature provided by the  Frappe Framework.
2025-08-29 11:56:42 +02:00
Marc-Constantin Enke
00fd1d2f26 feat: add permission check for custom button 2025-08-29 11:09:22 +02:00
Marc-Constantin Enke
7f55f421ab fix: remove ignore_permissions 2025-08-29 11:07:37 +02:00
MochaMind
781c377588 fix: Norwegian Bokmal translations 2025-08-29 03:50:30 +05:30
MochaMind
7318c6007d fix: Danish translations 2025-08-29 03:50:27 +05:30
ravibharathi656
e8288a2f63 fix: run config with force 2025-08-28 19:41:13 +05:30
l0gesh29
afb067ce50 fix: ignore cancelled gl and add company filter 2025-08-28 19:16:10 +05:30
MochaMind
e36cc5641c fix: Norwegian Bokmal translations 2025-08-28 03:55:18 +05:30
MochaMind
a98eb60a27 fix: Danish translations 2025-08-28 03:55:14 +05:30
Navin-S-R
49bb095152 fix: show company currency symbol 2025-08-26 20:29:24 +05:30
Vignesh Sekar
4d3ddeae8d perf: check PCV (smaller) table before checking GL Entries 2025-08-26 00:14:15 +05:30
ravibharathi656
e5affb16c7 fix(exchange rate revaluation): add check for gain_loss 2025-08-25 21:12:26 +05:30
Bhavan23
770d6dd8e2 fix(budget): always set fiscal year before fetching company approver role 2025-08-25 11:14:44 +00:00
121 changed files with 28223 additions and 23500 deletions

View File

@@ -10,8 +10,10 @@ from frappe.contacts.doctype.address.address import (
class ERPNextAddress(Address):
def validate(self):
self.validate_reference()
self.update_compnay_address()
super().validate()
self.update_company_address()
if hasattr(super(), "validate"):
super().validate()
def link_address(self):
"""Link address based on owner"""
@@ -20,7 +22,7 @@ class ERPNextAddress(Address):
return super().link_address()
def update_compnay_address(self):
def update_company_address(self):
for link in self.get("links"):
if link.link_doctype == "Company":
self.is_your_company_address = 1
@@ -38,6 +40,10 @@ class ERPNextAddress(Address):
"""
After Address is updated, update the related 'Primary Address' on Customer.
"""
if hasattr(super(), "on_update"):
super().on_update()
address_display = get_address_display(self.as_dict())
filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)

View File

@@ -11,6 +11,9 @@
"cost_center",
"debit",
"credit",
"reporting_currency_exchange_rate",
"debit_in_reporting_currency",
"credit_in_reporting_currency",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
@@ -124,12 +127,30 @@
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
},
{
"fieldname": "debit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "credit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "reporting_currency_exchange_rate",
"fieldtype": "Float",
"label": "Reporting Currency Exchange Rate",
"precision": "9"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2024-03-27 13:05:56.710541",
"modified": "2025-08-22 19:13:50.400404",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
@@ -158,7 +179,8 @@
"role": "Auditor"
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -2,12 +2,15 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr
from frappe.utils import cint, cstr, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
from erpnext.setup.utils import get_exchange_rate
class AccountClosingBalance(Document):
@@ -26,12 +29,15 @@ class AccountClosingBalance(Document):
cost_center: DF.Link | None
credit: DF.Currency
credit_in_account_currency: DF.Currency
credit_in_reporting_currency: DF.Currency
debit: DF.Currency
debit_in_account_currency: DF.Currency
debit_in_reporting_currency: DF.Currency
finance_book: DF.Link | None
is_period_closing_voucher_entry: DF.Check
period_closing_voucher: DF.Link | None
project: DF.Link | None
reporting_currency_exchange_rate: DF.Float
# end: auto-generated types
pass
@@ -55,6 +61,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
"closing_date": closing_date,
}
)
set_amount_in_reporting_currency(cle, company, closing_date)
cle.flags.ignore_permissions = True
cle.flags.ignore_links = True
cle.submit()
@@ -144,3 +151,29 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = query.run(as_dict=1)
return entries
def set_amount_in_reporting_currency(cle, company, closing_date):
default_currency, reporting_currency = frappe.get_cached_value(
"Company", company, ["default_currency", "reporting_currency"]
)
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
if not reporting_currency_exchange_rate:
frappe.throw(
title=_("Reporting Currency Exchange Not Found"),
msg=_(
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
).format(default_currency, reporting_currency, closing_date),
exc=ReportingCurrencyExchangeNotFoundError,
)
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
cle.update(
{
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
"debit_in_reporting_currency": debit_in_reporting_currency,
"credit_in_reporting_currency": credit_in_reporting_currency,
}
)

View File

@@ -17,6 +17,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_8",
"rate",
"section_break_9",
@@ -95,6 +96,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"

View File

@@ -132,7 +132,8 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "IBAN",
"length": 30
"length": 34,
"options": "IBAN"
},
{
"fieldname": "column_break_12",
@@ -208,6 +209,7 @@
"label": "Disabled"
}
],
"grid_page_length": 50,
"links": [
{
"group": "Transactions",
@@ -250,7 +252,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2024-10-30 09:41:14.113414",
"modified": "2025-08-29 12:32:01.081687",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -282,9 +284,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "bank,account",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -52,7 +52,6 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
self.validate_account()
self.update_default_bank_account()
@@ -72,35 +71,6 @@ class BankAccount(Document):
if self.is_company_account and not self.company:
frappe.throw(_("Company is mandatory for company account"))
def validate_iban(self):
"""
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
"""
# IBAN field is optional
if not self.iban:
return
def encode_char(c):
# Position in the alphabet (A=1, B=2, ...) plus nine
return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord()
iban = "".join(self.iban.split(" ")).upper()
# Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4]
# Encode characters as numbers
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try:
to_check = int("".join(encoded))
except ValueError:
frappe.throw(_("IBAN is not valid"))
if to_check % 97 != 1:
frappe.throw(_("IBAN is not valid"))
def update_default_bank_account(self):
if self.is_default and not self.disabled:
frappe.db.set_value(

View File

@@ -8,38 +8,4 @@ from frappe.tests import IntegrationTestCase
class TestBankAccount(IntegrationTestCase):
def test_validate_iban(self):
valid_ibans = [
"GB82 WEST 1234 5698 7654 32",
"DE91 1000 0000 0123 4567 89",
"FR76 3000 6000 0112 3456 7890 189",
]
invalid_ibans = [
# wrong checksum (3rd place)
"GB72 WEST 1234 5698 7654 32",
"DE81 1000 0000 0123 4567 89",
"FR66 3000 6000 0112 3456 7890 189",
]
bank_account = frappe.get_doc({"doctype": "Bank Account"})
try:
bank_account.validate_iban()
except AttributeError:
msg = "BankAccount.validate_iban() failed for empty IBAN"
self.fail(msg=msg)
for iban in valid_ibans:
bank_account.iban = iban
try:
bank_account.validate_iban()
except ValidationError:
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
self.fail(msg=msg)
for not_iban in invalid_ibans:
bank_account.iban = not_iban
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban()
pass

View File

@@ -155,8 +155,10 @@ def get_payment_entries_for_bank_clearance(
entries = []
condition = ""
pe_condition = ""
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(
f"""
@@ -181,19 +183,20 @@ def get_payment_entries_for_bank_clearance(
payment_entries = frappe.db.sql(
f"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
"Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
from `tabPayment Entry` as pe
join `tabCompany` c on c.name = pe.company
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
{pe_condition}
order by
posting_date ASC, name DESC
pe.posting_date ASC, pe.name DESC
""",
{
"account": account,

View File

@@ -146,6 +146,7 @@
"fieldname": "iban",
"fieldtype": "Data",
"label": "IBAN",
"options": "IBAN",
"read_only": 1
},
{
@@ -214,9 +215,10 @@
"read_only": 1
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:06:37.731207",
"modified": "2025-08-29 11:52:33.550847",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",
@@ -250,9 +252,10 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "customer",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "customer"
}
}

View File

@@ -76,6 +76,18 @@ class BankStatementImport(DataImport):
self.validate_google_sheets_url()
def start_import(self):
"""
Start a background import job for this Bank Statement Import.
Validates that the preview contains a "Bank Account" column and that the scheduler is active (unless running in test or developer mode). If validation passes and there is not already an enqueued job for this document, enqueue a background worker to perform the import.
Returns:
str | None: The enqueued job_id when a new job was queued, otherwise None.
Raises:
frappe.ValidationError: If the preview is missing a "Bank Account" column.
frappe.ValidationError: If the scheduler is inactive and import is not allowed to run immediately.
"""
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
self.import_file, self.google_sheets_url
)
@@ -111,20 +123,94 @@ class BankStatementImport(DataImport):
return None
def preprocess_mt940_content(content: str) -> str:
"""
Truncate overly long MT940 statement numbers found in `:28C:` tags to the last 5 digits.
This function fixes MT940 files where banks supply statement numbers longer than the MT940-expected maximum (5 digits),
which can break parsers. It only processes lines that start with the `:28C:` tag and:
- leaves content unchanged if no `:28C:` tag is present,
- truncates numeric statement numbers longer than 5 digits to their last 5 digits,
- preserves any `/sequence` suffix and trailing whitespace on the same line.
Parameters:
content (str): Raw MT940 file content.
Returns:
str: The processed content with corrected `:28C:` statement numbers.
"""
# Fast-path: bail if no :28C: tag exists
if ":28C:" not in content:
return content
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
pattern = re.compile(r'(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$')
def replace_statement_number(match):
"""
Replace a matched MT940 :28C: statement number by truncating it to the last five digits if it is longer.
Parameters:
match (re.Match): A regex match with groups:
1: prefix (e.g., ':28C:')
2: numeric statement number
3: optional sequence part (e.g., '/1')
4: optional trailing whitespace
Returns:
str: Reconstructed replacement string preserving prefix, (possibly truncated) statement number, sequence part, and trailing whitespace.
"""
prefix = match.group(1) # ':28C:'
statement_num = match.group(2) # The statement number
sequence_part = match.group(3) or '' # The sequence part like '/1'
trailing_space = match.group(4) or '' # Preserve trailing whitespace
# If statement number is longer than 5 digits, truncate to last 5 digits
if len(statement_num) > 5:
statement_num = statement_num[-5:]
return prefix + statement_num + sequence_part + trailing_space
# Apply the replacement
processed_content = pattern.sub(replace_statement_number, content)
return processed_content
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
"""
Convert an MT940 file to a CSV and save it to the Frappe File Manager, returning the saved file URL.
This function:
- Loads the specified MT940 file, verifies it is MT940 format, preprocesses content to fix statement number formatting, and parses transactions.
- Writes parsed transactions to an in-memory CSV with headers: Date, Deposit, Withdrawal, Description, Reference Number, Bank Account, Currency.
- Saves the CSV as a private attachment on the Bank Statement Import document and returns the file URL.
Parameters:
data_import (str): Name (docname) of the Bank Statement Import document to attach the converted CSV to.
mt940_file_path (str): File path or file identifier pointing to the uploaded MT940 file to convert.
Returns:
str: URL of the saved CSV file in the File Manager.
Raises:
frappe.ValidationError: If the file is not MT940, MT940 import is not enabled on the document, parsing fails, or no transactions are found.
"""
doc = frappe.get_doc("Bank Statement Import", data_import)
file_doc, content = get_file(mt940_file_path)
if not is_mt940_format(content):
is_mt940 = is_mt940_format(content)
if not is_mt940:
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
if is_mt940_format(content) and not doc.import_mt940_fromat:
if is_mt940 and not doc.import_mt940_fromat:
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
try:
transactions = mt940.parse(content)
# Preprocess MT940 content to fix statement number format issues
processed_content = preprocess_mt940_content(content)
transactions = mt940.parse(processed_content)
except Exception as e:
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
@@ -249,6 +335,20 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
def update_mapping_db(bank, template_options):
"""
Update a Bank document's transaction field mappings to match the provided template options.
This replaces all existing entries in the Bank.bank_transaction_mapping child table with mappings from
the JSON-encoded template_options. The expected template_options JSON contains a "column_to_field_map"
object mapping file column names (keys) to bank transaction field names (values).
Parameters:
bank (str | frappe.model.document.Document): Bank name/docname or a Bank document.
template_options (str): JSON string containing a "column_to_field_map" mapping of file column -> bank field.
Side effects:
Overwrites the Bank.bank_transaction_mapping entries and saves the Bank document.
"""
bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping:
d.delete()
@@ -260,6 +360,17 @@ def update_mapping_db(bank, template_options):
def add_bank_account(data, bank_account):
"""
Ensure every data row contains the given bank account value.
Assumes `data` is a list of rows where data[0] is the header row. If the header row does not contain "Bank Account",
this function appends that header and appends the `bank_account` value to each subsequent row. If the header exists,
it sets the `bank_account` value into the existing "Bank Account" column for every data row. Mutates `data` in place.
Parameters:
data (list[list]): Table-like data with the first row as headers.
bank_account (str): Bank account value to set for each data row.
"""
bank_account_loc = None
if "Bank Account" not in data[0]:
data[0].append("Bank Account")
@@ -276,6 +387,21 @@ def add_bank_account(data, bank_account):
def write_files(import_file, data):
"""
Write processed tabular data back to the original import file path (CSV or Excel).
This function overwrites the file referenced by import_file.file_doc.get_full_path().
- If the file extension is "csv", writes rows using the csv writer (expects `data` as an iterable of row iterables).
- If the extension is "xlsx" or "xls", writes to an Excel workbook using write_xlsx with sheet name "trans".
Parameters:
import_file: object
File wrapper whose `.file_doc.get_full_path()` and `.file_doc.get_extension()` are used to determine the target path and extension.
data: Iterable[Iterable]
Sequence of rows (each row is an iterable of cell values) to be written.
No return value.
"""
full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension()
extension = parts[1]
@@ -285,11 +411,26 @@ def write_files(import_file, data):
with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file)
writer.writerows(data)
elif extension == "xlsx" or "xls":
elif extension in ("xlsx", "xls"):
write_xlsx(data, "trans", file_path=full_file_path)
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
"""
Write rows of data to an Excel worksheet and save the workbook.
Creates a sheet named `sheet_name` in the provided openpyxl workbook (or a new write-only workbook if `wb` is None), applies optional column widths, converts HTML in string cells (except for sheets named "Data Import Template" or "Data Export"), strips characters illegal in Excel, and saves the workbook to `file_path`.
Parameters:
data (Iterable[Sequence]): Iterable of rows, where each row is a sequence of cell values.
sheet_name (str): Name of the worksheet to create.
wb (openpyxl.Workbook, optional): Workbook to append the sheet to. If not provided, a new write-only Workbook is created.
column_widths (Sequence[Number], optional): Sequence of column widths; indexes correspond to columns starting at 1.
file_path (str): File path where the workbook will be saved.
Returns:
bool: True on successful save.
"""
# from xlsx utils with changes
column_widths = column_widths or []
if wb is None:

View File

@@ -1,10 +1,220 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
preprocess_mt940_content,
is_mt940_format,
)
class TestBankStatementImport(IntegrationTestCase):
pass
class TestBankStatementImport(unittest.TestCase):
"""Unit tests for Bank Statement Import functions"""
def test_preprocess_mt940_content_with_long_statement_number(self):
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
# Test case with 6-digit statement number (167619 -> 67619)
mt940_content = ":28C:167619/1"
expected_content = ":28C:67619/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_with_normal_statement_number(self):
"""Test that statement numbers with 5 or fewer digits are unchanged"""
# Test case with 5-digit statement number (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
# Test case with 4-digit statement number (should remain unchanged)
mt940_content = ":28C:1234/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
def test_preprocess_mt940_content_without_sequence_number(self):
"""Test statement number truncation without sequence number"""
# Test case with long statement number but no sequence (no /1)
mt940_content = ":28C:987654321"
expected_content = ":28C:54321"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_multiple_occurrences(self):
"""Test multiple statement numbers in the same content"""
mt940_content = """:28C:167619/1
:28C:987654/2"""
expected_content = """:28C:67619/1
:28C:87654/2"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_edge_cases(self):
"""Test edge cases like empty content and content without :28C: tags"""
# Test empty content
self.assertEqual(preprocess_mt940_content(""), "")
# Test content without :28C: tags
content_without_28c = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
result = preprocess_mt940_content(content_without_28c)
self.assertEqual(result, content_without_28c) # Should be unchanged
def test_preprocess_mt940_content_with_full_mt940_document(self):
"""Test preprocessing with complete MT940 document"""
mt940_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
expected_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:67619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_is_mt940_format_detection(self):
"""Test MT940 format detection function"""
# Valid MT940 content with all required tags
valid_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
self.assertTrue(is_mt940_format(valid_mt940))
# Invalid MT940 content (CSV format)
invalid_mt940 = """Date,Description,Amount
2023-01-01,Test Transaction,100.00
2023-01-02,Another Transaction,-50.00"""
self.assertFalse(is_mt940_format(invalid_mt940))
# Partially valid MT940 (missing some required tags)
partial_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
self.assertFalse(is_mt940_format(partial_mt940))
# Empty content
self.assertFalse(is_mt940_format(""))
def test_preprocess_mt940_content_boundary_conditions(self):
"""
Verify preprocessing handles statement-number length boundaries in `:28C:` tags.
Checks that:
- A 6-digit statement number is truncated to its last 5 digits.
- A 5-digit statement number remains unchanged.
- A very long statement number is reduced to its last 5 digits.
"""
# Test exactly 6 digits (should be truncated)
mt940_content = ":28C:123456/1"
expected_content = ":28C:23456/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test exactly 5 digits (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content)
# Test very long statement number
mt940_content = ":28C:123456789012345/1"
expected_content = ":28C:12345/1" # Last 5 digits
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_real_world_case(self):
"""
Verify preprocessing of a real-world MT940 document: truncate 6-digit `:28C:` statement numbers to their last 5 digits and preserve all other content.
Uses a sanitized, production-failing MT940 sample where `:28C:167619/1` must become `:28C:67619/1`. Asserts the entire document matches the expected transformed output, that the truncated tag is present and the original is absent, and that unrelated fields (e.g., `:20:` reference and UPI details) remain unchanged.
"""
# This is based on actual MT940 content that was causing parsing errors (sanitized)
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619
:25:1234567890
:28C:167619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
# Expected result with statement number 167619 truncated to 67619
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619
:25:1234567890
:28C:67619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Verify that the problematic statement number was actually changed
self.assertIn(":28C:67619/1", result)
self.assertNotIn(":28C:167619/1", result)
# Verify that other content remains unchanged
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces
mt940_content = ":28C:167619/1 \n"
expected_content = ":28C:67619/1 \n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with Windows line endings (CRLF)
mt940_content = ":28C:167619/1\r\n"
expected_content = ":28C:67619/1\r\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with leading spaces (should not match as it's not line start)
mt940_content = " :28C:167619/1\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should remain unchanged

View File

@@ -223,7 +223,8 @@
{
"fieldname": "bank_party_iban",
"fieldtype": "Data",
"label": "Party IBAN (Bank Statement)"
"label": "Party IBAN (Bank Statement)",
"options": "IBAN"
},
{
"fieldname": "bank_party_account_number",
@@ -238,7 +239,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-06-18 17:24:57.044666",
"modified": "2025-08-29 11:53:45.908169",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -7,6 +7,9 @@ from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
IBAN_1 = "DE02000000003716541159"
IBAN_2 = "DE02500105170137075030"
class TestAutoMatchParty(IntegrationTestCase):
@classmethod
@@ -22,24 +25,24 @@ class TestAutoMatchParty(IntegrationTestCase):
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self):
create_supplier_for_match(account_no="000000003716541159")
create_supplier_for_match(account_no=IBAN_1[11:])
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
account_no="000000003716541159",
iban="DE02000000003716541159",
account_no=IBAN_1[11:],
iban=IBAN_1,
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_iban(self):
create_supplier_for_match(iban="DE02000000003716541159")
create_supplier_for_match(iban=IBAN_1)
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="c5455a224602afaa51592a9d9250600d",
account_no="000000003716541159",
iban="DE02000000003716541159",
account_no=IBAN_1[11:],
iban=IBAN_1,
)
self.assertEqual(doc.party_type, "Supplier")
@@ -51,7 +54,7 @@ class TestAutoMatchParty(IntegrationTestCase):
withdrawal=1200,
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
party_name="Ella Jackson",
iban="DE04000000003716545346",
iban=IBAN_2,
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Jackson Ella W.")

View File

@@ -145,8 +145,10 @@ def validate_expense_against_budget(args, expense_amount=0):
if not frappe.db.count("Budget", cache=True):
return
if args.get("company") and not args.fiscal_year:
if not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
if args.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", args.get("company"), "exception_budget_approver_role"
)

View File

@@ -134,7 +134,8 @@ class ExchangeRateRevaluation(Document):
accounts = self.get_accounts_data()
if accounts:
for acc in accounts:
self.append("accounts", acc)
if acc.get("gain_loss"):
self.append("accounts", acc)
@frappe.whitelist()
def get_accounts_data(self):

View File

@@ -29,14 +29,17 @@
"against_voucher",
"voucher_detail_no",
"transaction_exchange_rate",
"reporting_currency_exchange_rate",
"amounts_section",
"debit_in_account_currency",
"debit",
"debit_in_transaction_currency",
"debit_in_reporting_currency",
"column_break_bm1w",
"credit_in_account_currency",
"credit",
"credit_in_transaction_currency",
"credit_in_reporting_currency",
"dimensions_section",
"cost_center",
"column_break_lmnm",
@@ -353,13 +356,31 @@
{
"fieldname": "column_break_8abq",
"fieldtype": "Column Break"
},
{
"fieldname": "debit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "credit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "reporting_currency_exchange_rate",
"fieldtype": "Float",
"label": "Reporting Currency Exchange Rate",
"precision": "9"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2025-03-21 15:29:11.221890",
"modified": "2025-08-22 12:57:17.750252",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
@@ -390,8 +411,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -19,7 +19,8 @@ from erpnext.accounts.party import (
validate_party_gle_currency,
)
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
from erpnext.setup.utils import get_exchange_rate
exclude_from_linked_with = True
@@ -42,9 +43,11 @@ class GLEntry(Document):
cost_center: DF.Link | None
credit: DF.Currency
credit_in_account_currency: DF.Currency
credit_in_reporting_currency: DF.Currency
credit_in_transaction_currency: DF.Currency
debit: DF.Currency
debit_in_account_currency: DF.Currency
debit_in_reporting_currency: DF.Currency
debit_in_transaction_currency: DF.Currency
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -57,6 +60,7 @@ class GLEntry(Document):
posting_date: DF.Date | None
project: DF.Link | None
remarks: DF.Text | None
reporting_currency_exchange_rate: DF.Float
to_rename: DF.Check
transaction_currency: DF.Link | None
transaction_date: DF.Date | None
@@ -88,6 +92,8 @@ class GLEntry(Document):
self.validate_party()
self.validate_currency()
self.set_amount_in_reporting_currency()
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
@@ -131,18 +137,20 @@ class GLEntry(Document):
if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
# skipping validation for payroll entry creation in case party is not required
if not frappe.flags.party_not_required_for_receivable_payable:
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
)
# Zero value transaction is not allowed
if not (
@@ -292,6 +300,25 @@ class GLEntry(Document):
if self.party_type and self.party:
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
def set_amount_in_reporting_currency(self):
default_currency, reporting_currency = frappe.get_cached_value(
"Company", self.company, ["default_currency", "reporting_currency"]
)
transaction_date = self.transaction_date or self.posting_date
self.reporting_currency_exchange_rate = get_exchange_rate(
default_currency, reporting_currency, transaction_date
)
if not self.reporting_currency_exchange_rate:
frappe.throw(
title=_("Reporting Currency Exchange Not Found"),
msg=_(
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
).format(default_currency, reporting_currency, transaction_date),
exc=ReportingCurrencyExchangeNotFoundError,
)
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
def validate_and_set_fiscal_year(self):
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
@@ -308,7 +335,7 @@ def validate_balance_type(account, adv_adj=False):
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where account = %s""",
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
account,
)[0][0]

View File

@@ -644,8 +644,11 @@ class JournalEntry(AccountsController):
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type")
# skipping validation for payroll entry creation
skip_validation = frappe.flags.party_not_required_for_receivable_payable
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party):
if not (d.party_type and d.party) and not skip_validation:
frappe.throw(
_(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"

View File

@@ -25,6 +25,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"help_section",
"loyalty_program_help"
],
@@ -144,6 +145,12 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"links": [],

View File

@@ -14,6 +14,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_4",
"invoices"
],
@@ -63,6 +64,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",

View File

@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True)
party_link.save()
return party_link

View File

@@ -28,6 +28,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"sec_break1",
"invoice_name",
"invoices",
@@ -194,6 +195,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"depends_on": "eval:doc.party",
"description": "Only 'Payment Entries' made against this advance account are supported.",

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -392,6 +393,12 @@ class PaymentReconciliation(Document):
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
difference_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
)
difference_amount = 0
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
@@ -399,8 +406,14 @@ class PaymentReconciliation(Document):
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_ref_rate = flt(
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
allocated_amount_in_inv_rate = flt(
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount

View File

@@ -228,7 +228,8 @@
"fetch_from": "bank_account.iban",
"fieldname": "iban",
"fieldtype": "Read Only",
"label": "IBAN"
"label": "IBAN",
"options": "IBAN"
},
{
"fetch_from": "bank_account.branch_code",
@@ -458,11 +459,12 @@
"label": "Phone Number"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-04 05:39:32.448857",
"modified": "2025-08-29 11:52:48.555415",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -497,8 +499,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -75,6 +75,17 @@ class PeriodClosingVoucher(AccountsController):
return
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if previous_fiscal_year_closed:
return
gle_exists_in_previous_year = frappe.db.exists(
"GL Entry",
{
@@ -86,16 +97,7 @@ class PeriodClosingVoucher(AccountsController):
if not gle_exists_in_previous_year:
return
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if not previous_fiscal_year_closed:
frappe.throw(_("Previous Year is not closed, please close it first"))
frappe.throw(_("Previous Year is not closed, please close it first"))
def block_if_future_closing_voucher_exists(self):
future_closing_voucher = self.get_future_closing_voucher()

View File

@@ -83,6 +83,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
},
};
});
frm.set_query("print_format", function () {
return {
filters: {
print_format_for: "Report",
report: frm.doc.report,
disabled: 0,
print_format_type: "Jinja",
},
};
});
if (frm.doc.__islocal) {
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value("to_date", frappe.datetime.get_today());
@@ -106,6 +116,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
filters: filters,
};
});
frm.set_query("print_format", function () {
return {
filters: {
print_format_for: "Report",
report: frm.doc.report,
disabled: 0,
print_format_type: "Jinja",
},
};
});
},
customer_collection: function (frm) {
frm.set_value("collection_name", "");

View File

@@ -38,6 +38,7 @@
"column_break_17",
"customers",
"preferences",
"print_format",
"orientation",
"include_break",
"include_ageing",
@@ -406,10 +407,16 @@
"fieldname": "show_future_payments",
"fieldtype": "Check",
"label": "Show Future Payments"
},
{
"fieldname": "print_format",
"fieldtype": "Link",
"label": "Print Format",
"options": "Print Format"
}
],
"links": [],
"modified": "2025-08-29 00:20:08.088189",
"modified": "2025-09-03 14:24:43.608565",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -67,6 +67,7 @@ class ProcessStatementOfAccounts(Document):
pdf_name: DF.Data | None
posting_date: DF.Date | None
primary_mandatory: DF.Check
print_format: DF.Link | None
project: DF.TableMultiSelect[PSOAProject]
report: DF.Literal["General Ledger", "Accounts Receivable"]
sales_partner: DF.Link | None
@@ -109,6 +110,25 @@ class ProcessStatementOfAccounts(Document):
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
if self.print_format:
pf = frappe.db.get_value(
"Print Format",
self.print_format,
["print_format_type", "print_format_for", "report", "disabled"],
as_dict=True,
)
if not pf:
frappe.throw(title=_("Invalid Print Format"), msg=_("Selected Print Format does not exist."))
if pf.print_format_type != "Jinja":
frappe.throw(title=_("Invalid Print Format"), msg=_("Print Format Type should be Jinja."))
if pf.print_format_for != "Report" or pf.report != self.report or pf.disabled:
frappe.throw(
title=_("Invalid Print Format"),
msg=_(
"Print Format must be an enabled Report Print Format matching the selected Report."
),
)
def validate_account(self):
if not self.account:
return
@@ -290,6 +310,10 @@ def get_html(doc, filters, entry, col, res, ageing):
if process_soa_html and process_soa_html.get(doc.report):
template_path = process_soa_html[doc.report][-1]
if doc.print_format:
custom_html, custom_css = frappe.db.get_value("Print Format", doc.print_format, ["html", "css"])
template_path = f"<style>{custom_css}</style> {custom_html}"
if doc.letter_head:
from frappe.www.printview import get_letter_head

View File

@@ -328,11 +328,20 @@
<td></td>
<td></td>
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
{% if not(filters.show_future_payments) %}
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
{% else %}
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
{% endif %}
</tbody>
</table>
<br>

View File

@@ -23,6 +23,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_9",
"account_currency",
"net_amount",
@@ -214,6 +215,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",

View File

@@ -275,6 +275,7 @@
"read_only": 1
},
{
"fetch_from": "customer.tax_id",
"fieldname": "tax_id",
"fieldtype": "Data",
"hide_days": 1,
@@ -2241,7 +2242,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-08-04 19:20:28.732039",
"modified": "2025-09-09 14:48:59.472826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -17,6 +17,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_8",
"rate",
"section_break_9",
@@ -191,6 +192,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",

View File

@@ -18,6 +18,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"shipping_amount_section",
"calculate_based_on",
"column_break_8",
@@ -138,6 +139,12 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"icon": "fa fa-truck",

View File

@@ -976,6 +976,7 @@ class ReceivablePayableReport:
if self.account_type == "Receivable":
self.add_customer_filters()
self.exclude_employee_transaction()
elif self.account_type == "Payable":
self.add_supplier_filters()
@@ -1055,6 +1056,9 @@ class ReceivablePayableReport:
)
)
def exclude_employee_transaction(self):
self.qb_selection_filter.append(self.ple.party_type != "Employee")
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):

View File

@@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
get_report_summary as get_pl_summary,
)
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
from erpnext.accounts.utils import get_zero_cutoff
def execute(filters=None):
@@ -564,7 +565,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row[company] = flt(d.get(company, 0.0), 3)
if abs(row[company]) >= 0.005:
if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency):
# ignore zero values
has_value = True
total += flt(row[company])

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
)
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from erpnext.accounts.utils import get_zero_cutoff
def execute(filters=None):
@@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
for dimension in dimension_list:
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
if abs(row[frappe.scrub(dimension)]) >= 0.005:
if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimension_with_children,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff
def get_period_list(
@@ -306,7 +306,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
row[period.key] = flt(d.get(period.key, 0.0), 3)
if abs(row[period.key]) >= 0.005:
if abs(row[period.key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True
total += flt(row[period.key])
@@ -530,6 +530,7 @@ def get_accounting_entries(
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
query = query.force_index("posting_date_company_index")
if ignore_opening_entries and not ignore_is_opening:
query = query.where(gl_entry.is_opening == "No")

View File

@@ -667,7 +667,7 @@ def get_columns(filters):
"options": "GL Entry",
"hidden": 1,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
{
"label": _("Account"),
"fieldname": "account",

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
)
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from erpnext.accounts.utils import get_zero_cutoff
value_fields = ("income", "expense", "gross_profit_loss")
@@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= 0.005:
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True

View File

@@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import (
set_gl_entries_by_account,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_zero_cutoff
value_fields = (
"opening_debit",
@@ -412,7 +413,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= 0.005:
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True

View File

@@ -4,15 +4,25 @@
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory": 1,
"options": "Company",
"wildcard_filter": 0
}
],
"idx": 0,
"is_standard": "Yes",
"modified": "2019-01-17 17:20:42.374958",
"modified": "2025-08-28 19:06:54.273322",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance (Simple)",
"owner": "Administrator",
"prepared_report": 0,
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance (Simple)",
"report_type": "Query Report",

View File

@@ -10,6 +10,7 @@ from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
get_zero_cutoff,
sort_stock_vouchers_by_posting_date,
)
from erpnext.stock.doctype.item.test_item import make_item
@@ -157,6 +158,11 @@ class TestUtils(IntegrationTestCase):
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
frappe.db.set_default("supp_master_name", "Supplier Name")
def test_get_zero_cutoff(self):
self.assertEqual(get_zero_cutoff(None), 0.005)
self.assertEqual(get_zero_cutoff("EUR"), 0.005)
self.assertEqual(get_zero_cutoff("BHD"), 0.0005)
ADDRESS_RECORDS = [
{

View File

@@ -27,6 +27,7 @@ from frappe.utils import (
now,
nowdate,
)
from frappe.utils.caching import site_cache
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
@@ -1147,6 +1148,29 @@ def get_currency_precision():
return precision
def get_fraction_units(currency: str) -> int:
"""Returns the number of fraction units for a currency."""
fraction_units = frappe.db.get_value("Currency", currency, "fraction_units")
if fraction_units is None:
fraction_units = 100
return fraction_units
@site_cache()
def get_zero_cutoff(currency: str) -> float:
"""Returns the zero cutoff for a currency.
For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005.
We don't want to display values less than the zero cutoff.
This value was chosen for compatibility with the previous hard-coded value of 0.005.
"""
fraction_units = get_fraction_units(currency)
return 0.5 / (fraction_units or 1)
def get_held_invoices(party_type, party):
"""
Returns a list of names Purchase Invoices for the given party that are on hold
@@ -2482,6 +2506,10 @@ def build_qb_match_conditions(doctype, user=None) -> list:
for filter in match_filters:
for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, [])
cond = None
if link_option == doctype:
cond = _dt["name"].isin(allowed_values)
for fieldname in fieldnames:
field = _dt[fieldname]
@@ -2490,6 +2518,7 @@ def build_qb_match_conditions(doctype, user=None) -> list:
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
if cond:
criterion.append(cond)
return criterion

View File

@@ -41,6 +41,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"target_fixed_asset_account"
],
"fields": [
@@ -275,6 +276,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"

View File

@@ -18,6 +18,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"fixed_asset_account"
],
"fields": [
@@ -98,6 +99,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",

View File

@@ -61,7 +61,7 @@ class AssetMovement(Document):
if d.source_location:
if current_location != d.source_location:
frappe.throw(
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
_("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location)
)
else:
d.source_location = current_location
@@ -76,11 +76,11 @@ class AssetMovement(Document):
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
)
def validate_employee(self, d):
if self.purpose == "Tranfer and Issue":
if self.purpose == "Transfer and Issue":
if not d.from_employee:
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
@@ -89,7 +89,7 @@ class AssetMovement(Document):
if current_custodian != d.from_employee:
frappe.throw(
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
_("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee)
)
if not d.to_employee:
@@ -97,7 +97,7 @@ class AssetMovement(Document):
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
)
def on_submit(self):

View File

@@ -503,6 +503,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
onload() {
super.onload();
this.frm.set_query("supplier", function () {
return {
filters: {

View File

@@ -133,7 +133,10 @@ frappe.ui.form.on("Supplier", {
__("Actions")
);
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link")
) {
frm.add_custom_button(
__("Link with Customer"),
function () {

View File

@@ -1070,7 +1070,9 @@ class StockController(AccountsController):
from erpnext.stock.stock_ledger import make_sl_entries
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
update_batch_qty(
self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher
)
def make_gl_entries_on_cancel(self, from_repost=False):
if not from_repost:

View File

@@ -24,3 +24,7 @@ class InvalidAccountDimensionError(frappe.ValidationError):
class MandatoryAccountDimensionError(frappe.ValidationError):
pass
class ReportingCurrencyExchangeNotFoundError(frappe.ValidationError):
pass

View File

@@ -51,7 +51,7 @@ doctype_list_js = {
],
}
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
extend_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}

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

View File

@@ -1414,7 +1414,7 @@ def get_children(parent=None, is_root=False, **filters):
return bom_items
def add_additional_cost(stock_entry, work_order):
def add_additional_cost(stock_entry, work_order, job_card=None):
# Add non stock items cost in the additional cost
stock_entry.additional_costs = []
company_account = frappe.db.get_value(
@@ -1427,13 +1427,16 @@ def add_additional_cost(stock_entry, work_order):
expense_account = (
company_account.default_operating_cost_account or company_account.default_expense_account
)
add_non_stock_items_cost(stock_entry, work_order, expense_account)
add_operations_cost(stock_entry, work_order, expense_account)
add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=job_card)
add_operations_cost(stock_entry, work_order, expense_account, job_card=job_card)
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=None):
bom = frappe.get_doc("BOM", work_order.bom_no)
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
table = "items"
if work_order and not job_card:
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
items = {}
for d in bom.get(table):
@@ -1464,13 +1467,16 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
def add_operating_cost_component_wise(
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None, job_card=None
):
if not work_order:
return False
cost_added = False
for row in work_order.operations:
if job_card and job_card.operation_id != row.name:
continue
workstation_cost = frappe.get_all(
"Workstation Cost",
fields=["operating_component", "operating_cost"],
@@ -1511,14 +1517,14 @@ def get_component_account(parent):
return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account")
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit:
cost_added = add_operating_cost_component_wise(
stock_entry, work_order, operating_cost_per_unit, expense_account
stock_entry, work_order, operating_cost_per_unit, expense_account, job_card=job_card
)
if not cost_added:

View File

@@ -115,7 +115,8 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate"
"label": "Rate",
"options": "currency"
},
{
"columns": 1,
@@ -158,7 +159,8 @@
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
"read_only": 1,
"options": "currency"
},
{
"fieldname": "column_break_yuca",
@@ -180,13 +182,15 @@
"fieldname": "base_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Amount"
"label": "Base Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "base_rate",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Rate"
"label": "Base Rate",
"options": "Company:company:default_currency"
},
{
"default": "0",

View File

@@ -23,6 +23,7 @@ from frappe.utils import (
time_diff_in_hours,
)
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
get_mins_between_operations,
)
@@ -817,9 +818,6 @@ class JobCard(Document):
)
def update_work_order(self):
if self.track_semi_finished_goods:
return
if not self.work_order:
return
@@ -849,9 +847,9 @@ class JobCard(Document):
def update_semi_finished_good_details(self):
if self.operation_id:
frappe.db.set_value(
"Work Order Operation", self.operation_id, "completed_qty", self.manufactured_qty
)
qty = max(flt(self.manufactured_qty), flt(self.total_completed_qty))
frappe.db.set_value("Work Order Operation", self.operation_id, "completed_qty", qty)
if (
self.finished_good
and frappe.get_cached_value("Work Order", self.work_order, "production_item")
@@ -1322,6 +1320,9 @@ class JobCard(Document):
ste.make_stock_entry()
ste.stock_entry.flags.ignore_mandatory = True
wo_doc = frappe.get_doc("Work Order", self.work_order)
add_additional_cost(ste.stock_entry, wo_doc, self)
ste.stock_entry.save()
if auto_submit:

View File

@@ -24,6 +24,9 @@
"overproduction_percentage_for_sales_order",
"column_break_16",
"overproduction_percentage_for_work_order",
"section_break_xhtl",
"transfer_extra_materials_percentage",
"column_break_kemp",
"job_card_section",
"add_corrective_operation_cost_in_finished_good_valuation",
"enforce_time_logs",
@@ -243,13 +246,28 @@
"fieldname": "enforce_time_logs",
"fieldtype": "Check",
"label": "Enforce Time Logs"
},
{
"fieldname": "section_break_xhtl",
"fieldtype": "Section Break",
"label": "Extra Material Transfer"
},
{
"fieldname": "column_break_kemp",
"fieldtype": "Column Break"
},
{
"description": "The user will be able to transfer additional materials from the store to the Work in Progress (WIP) warehouse.",
"fieldname": "transfer_extra_materials_percentage",
"fieldtype": "Percent",
"label": "Transfer Extra Raw Materials to WIP (%)"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-16 11:23:16.916512",
"modified": "2025-09-08 19:48:31.726126",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -35,6 +35,7 @@ class ManufacturingSettings(Document):
overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
transfer_extra_materials_percentage: DF.Percent
update_bom_costs_automatically: DF.Check
validate_components_quantities_per_bom: DF.Check
# end: auto-generated types

View File

@@ -3005,6 +3005,36 @@ class TestWorkOrder(IntegrationTestCase):
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
)
def test_allow_additional_material_transfer(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
for row in wo_order.required_items:
make_stock_entry_test_record(
item_code=row.item_code,
target=row.source_warehouse,
qty=row.required_qty * 2,
basic_rate=100,
)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2))
stock_entry.insert()
stock_entry.submit()
wo_order.reload()
self.assertEqual(wo_order.material_transferred_for_manufacturing, 2)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
stock_entry.insert()
stock_entry.submit()
wo_order.reload()
self.assertEqual(wo_order.material_transferred_for_manufacturing, 3)
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -116,20 +116,6 @@ frappe.ui.form.on("Work Order", {
frm.set_indicator_formatter("operation", function (doc) {
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
});
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
args: {
bom_no: frm.doc.bom_no,
},
callback: function (r) {
if (r.message["set_scrap_wh_mandatory"]) {
frm.toggle_reqd("scrap_warehouse", true);
}
},
});
}
},
onload: function (frm) {
@@ -144,6 +130,20 @@ frappe.ui.form.on("Work Order", {
});
erpnext.work_order.set_default_warehouse(frm);
}
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
args: {
bom_no: frm.doc.bom_no,
},
callback: function (r) {
if (r.message["set_scrap_wh_mandatory"]) {
frm.toggle_reqd("scrap_warehouse", true);
}
},
});
}
},
source_warehouse: function (frm) {
@@ -732,6 +732,19 @@ erpnext.work_order = {
let pending_to_transfer = frm.doc.required_items.some(
(item) => flt(item.transferred_qty) < flt(item.required_qty)
);
let transfer_extra_materials_percentage =
frm.doc.__onload?.transfer_extra_materials_percentage;
let allowed_qty = 0;
let transfer_extra_materials = false;
if (!pending_to_transfer && transfer_extra_materials_percentage) {
allowed_qty = frm.doc.qty + (transfer_extra_materials_percentage / 100) * frm.doc.qty;
if (allowed_qty > frm.doc.material_transferred_for_manufacturing) {
transfer_extra_materials = true;
}
}
if (pending_to_transfer && frm.doc.status != "Stopped") {
frm.has_start_btn = true;
frm.add_custom_button(__("Create Pick List"), function () {
@@ -742,6 +755,14 @@ erpnext.work_order = {
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
});
start_btn.addClass("btn-primary");
} else if (transfer_extra_materials && allowed_qty) {
let qty = allowed_qty - flt(frm.doc.material_transferred_for_manufacturing);
if (qty > 0) {
frm.add_custom_button(__("Transfer Extra Material"), function () {
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture", qty);
});
}
}
}
}
@@ -967,19 +988,35 @@ erpnext.work_order = {
});
},
make_se: function (frm, purpose) {
this.show_prompt_for_qty_input(frm, purpose)
.then((data) => {
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
make_se: function (frm, purpose, qty) {
if (qty) {
frappe
.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: purpose,
qty: data.qty,
qty: qty,
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
} else {
this.show_prompt_for_qty_input(frm, purpose)
.then((data) => {
return frappe.xcall(
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
{
work_order_id: frm.doc.name,
purpose: purpose,
qty: data.qty,
}
);
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
}
},
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {

View File

@@ -149,6 +149,7 @@ class WorkOrder(Document):
self.set_onload("material_consumption", ms.material_consumption)
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
self.set_onload("transfer_extra_materials_percentage", ms.transfer_extra_materials_percentage)
self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
self.set_onload(
"enable_stock_reservation",
@@ -354,6 +355,10 @@ class WorkOrder(Document):
def calculate_operating_cost(self):
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
for d in self.get("operations"):
if not d.hour_rate:
if d.workstation:
d.hour_rate = get_hour_rate(d.workstation)
d.planned_operating_cost = flt(
flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
)
@@ -485,6 +490,13 @@ class WorkOrder(Document):
qty = self.get_transferred_or_manufactured_qty(purpose)
if not allowance_percentage and purpose == "Material Transfer for Manufacture":
allowance_percentage = flt(
frappe.db.get_single_value(
"Manufacturing Settings", "transfer_extra_materials_percentage"
)
)
completed_qty = self.qty + (allowance_percentage / 100 * self.qty)
if qty > completed_qty:
frappe.throw(
@@ -1408,8 +1420,8 @@ class WorkOrder(Document):
if not self.reserve_stock:
continue
wip_warehouse = wip_warehouse or item.source_warehouse
self.update_consumed_qty_in_stock_reservation(item, consumed_qty, wip_warehouse)
warehouse = wip_warehouse or item.source_warehouse
self.update_consumed_qty_in_stock_reservation(item, consumed_qty, warehouse)
def update_consumed_qty_in_stock_reservation(self, item, consumed_qty, wip_warehouse):
filters = {
@@ -2424,3 +2436,8 @@ def get_row_wise_serial_batch(work_order, purpose=None):
details.batch_nos[entry.batch_no] += abs(entry.qty)
return row_wise_serial_batch
@frappe.request_cache
def get_hour_rate(workstation):
return frappe.get_cached_value("Workstation", workstation, "hour_rate") or 0.0

View File

@@ -397,7 +397,7 @@ erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts
erpnext.patches.v15_0.refactor_closing_stock_balance #5
erpnext.patches.v15_0.update_asset_status_to_work_in_progress
erpnext.patches.v15_0.rename_manufacturing_settings_field
erpnext.patches.v15_0.sync_auto_reconcile_config
erpnext.patches.v15_0.sync_auto_reconcile_config #2025-08-26
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v14_0.update_posting_datetime
@@ -418,7 +418,7 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v15_0.remove_agriculture_roles
erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes
erpnext.patches.v15_0.drop_sle_indexes #2025-09-18
erpnext.patches.v15_0.update_pick_list_fields
execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1)
erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
@@ -436,3 +436,4 @@ erpnext.patches.v16_0.update_serial_no_reference_name
erpnext.patches.v16_0.set_invoice_type_in_pos_settings
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
erpnext.patches.v16_0.make_workstation_operating_components #1
erpnext.patches.v16_0.set_reporting_currency

View File

@@ -4,7 +4,7 @@ import frappe
def execute():
table = "tabStock Ledger Entry"
index_list = ["posting_datetime_creation_index", "item_warehouse"]
index_list = ["posting_datetime_creation_index", "item_warehouse", "batch_no_item_code_warehouse_index"]
for index in index_list:
if not frappe.db.has_index(table, index):

View File

@@ -11,7 +11,7 @@ def execute():
frappe.db.set_single_value("Accounts Settings", "reconciliation_queue_size", 5)
# Create Scheduler Event record if it doesn't exist
if frappe.reload_doc("core", "doctype", "scheduler_event"):
if frappe.reload_doc("core", "doctype", "scheduler_event", force=True):
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
if not frappe.db.get_all(
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}

View File

@@ -0,0 +1,159 @@
import frappe
from frappe.utils import getdate
from frappe.utils.nestedset import get_descendants_of
from erpnext.accounts.utils import get_fiscal_year
from erpnext.setup.utils import get_exchange_rate
def execute():
set_company_reporting_currency()
set_amounts_in_reporting_currency_on_gle_and_acb()
def set_company_reporting_currency():
root_companies = frappe.db.get_all(
"Company", fields=["name", "default_currency"], filters={"parent_company": ""}, order_by="lft"
)
for d in root_companies:
company_subtree = get_descendants_of("Company", d.name)
company_subtree.append(d.name)
update_company_subtree_reporting_currency(company_subtree, d.default_currency)
def update_company_subtree_reporting_currency(companies, currency):
Company = frappe.qb.DocType("Company")
frappe.qb.update(Company).set(Company.reporting_currency, currency).where(
Company.name.isin(companies)
).run()
def set_amounts_in_reporting_currency_on_gle_and_acb():
# get all the companies
companies = frappe.db.get_all(
"Company", fields=["name", "default_currency", "reporting_currency"], order_by="lft"
)
# get current fiscal year
current_fiscal_year = get_fiscal_year(getdate(), as_dict=1, raise_on_missing=False)
if not current_fiscal_year:
return
previous_fiscal_year = frappe.db.get_value(
"Fiscal Year",
filters={"year_end_date": ("<", current_fiscal_year.year_start_date)},
fieldname=["name", "year_start_date", "year_end_date"],
order_by="year_end_date desc",
as_dict=1,
)
for d in companies:
posting_dates = get_posting_closing_date(d, current_fiscal_year, previous_fiscal_year)
exchange_rate_available = check_exchange_rate_availability(d, posting_dates)
if not exchange_rate_available:
continue
set_reporting_currency_by_doctype("GL Entry", d, posting_dates.get("GL Entry"))
set_reporting_currency_by_doctype(
"Account Closing Balance", d, posting_dates.get("Account Closing Balance")
)
def get_posting_closing_date(company_details, current_fiscal_year, previous_fiscal_year=None):
posting_dates = {}
posting_dates["GL Entry"] = get_closing_posting_dates(
"GL Entry", company_details.get("name"), current_fiscal_year
)
posting_dates["Account Closing Balance"] = get_closing_posting_dates(
"Account Closing Balance", company_details.get("name"), current_fiscal_year
)
if previous_fiscal_year:
prev_fy_last_pcv_closing_date = frappe.db.get_value(
"Period Closing Voucher",
filters={"fiscal_year": previous_fiscal_year.name, "company": company_details.get("name")},
fieldname=["transaction_date"],
order_by="period_start_date desc",
)
if prev_fy_last_pcv_closing_date:
prev_fy_acb_closing_dates = get_closing_posting_dates(
"Account Closing Balance",
company_details.get("name"),
closing_date=prev_fy_last_pcv_closing_date,
)
posting_dates.setdefault("Account Closing Balance", [])
posting_dates["Account Closing Balance"].extend(prev_fy_acb_closing_dates)
return posting_dates
def check_exchange_rate_availability(company_details, posting_dates):
exchange_rate_available = True
for doctype, values in posting_dates.items():
if not exchange_rate_available:
return False
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
for d in values:
exchange_rate = get_exchange_rate(
company_details.get("default_currency"),
company_details.get("reporting_currency"),
d[date_column],
)
if not exchange_rate:
exchange_rate_available = False
break
return exchange_rate_available
def set_reporting_currency_by_doctype(doctype, company_details, posting_closing_dates):
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
for d in posting_closing_dates:
exchange_rate = get_exchange_rate(
company_details.get("default_currency"),
company_details.get("reporting_currency"),
d[date_column],
)
set_reporting_currency_on_individual_documents(
doctype, company_details.get("name"), d[date_column], exchange_rate
)
def get_closing_posting_dates(doctype, company, fiscal_year=None, closing_date=None):
dt = frappe.qb.DocType(doctype)
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
query = frappe.qb.from_(dt).select(dt[date_column]).where(dt.company == company).groupby(dt[date_column])
if doctype == "GL Entry" and fiscal_year:
query = query.where(dt.fiscal_year == fiscal_year.name)
if doctype == "Account Closing Balance":
if fiscal_year:
query = query.where(dt.closing_date[fiscal_year.year_start_date : fiscal_year.year_end_date])
if closing_date:
query = query.where(dt.closing_date == closing_date)
posting_closing_dates = query.run(as_dict=1)
return posting_closing_dates
def set_reporting_currency_on_individual_documents(doctype, company, posting_closing_date, exchange_rate):
dt = frappe.qb.DocType(doctype)
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
frappe.qb.update(dt).set(dt.reporting_currency_exchange_rate, exchange_rate).set(
dt.debit_in_reporting_currency, exchange_rate * dt.debit
).set(dt.credit_in_reporting_currency, exchange_rate * dt.credit).where(
(dt.company == company) & (dt[date_column] == posting_closing_date)
).run()

View File

@@ -1069,7 +1069,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer") {
(party_type = "Customer"), (party_name = me.frm.doc.party_name);
} else {
party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
party_type = frappe.meta.has_field(me.frm.doc.doctype, "supplier") ? "Supplier" : "Customer";
party_name = me.frm.doc[party_type.toLowerCase()];
}
if (party_name) {

View File

@@ -264,6 +264,5 @@ erpnext.setup.fiscal_years = {
Pakistan: ["07-01", "06-30"],
Singapore: ["04-01", "03-31"],
"South Africa": ["03-01", "02-28"],
Thailand: ["10-01", "09-30"],
"United Kingdom": ["04-01", "03-31"],
};

View File

@@ -190,7 +190,10 @@ frappe.ui.form.on("Customer", {
__("Actions")
);
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link")
) {
frm.add_custom_button(
__("Link with Supplier"),
function () {

View File

@@ -365,6 +365,7 @@
"fieldname": "base_net_rate",
"fieldtype": "Currency",
"label": "Net Rate (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -698,7 +699,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-06-12 17:31:47.775890",
"modified": "2025-08-26 20:31:47.775890",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -75,7 +75,7 @@ def create_demo_company():
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
frappe.db.set_default("company", new_company.name)
bank_account = create_bank_account({"company_name": new_company.name})
bank_account = create_bank_account({"company_name": new_company.name}, demo=True)
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
return new_company.name

View File

@@ -17,6 +17,9 @@ frappe.ui.form.on("Company", {
frm.toggle_enable("default_currency", !r.message);
});
}
if (frm.doc.__islocal) {
frm.set_value("reporting_currency", "");
}
},
setup: function (frm) {
frm.__rename_queue = "long";
@@ -156,6 +159,10 @@ frappe.ui.form.on("Company", {
}
}
if (frm.doc.__islocal) {
frm.set_value("reporting_currency", "");
}
erpnext.company.set_chart_of_accounts_options(frm.doc);
},

View File

@@ -22,6 +22,7 @@
"domain",
"date_of_establishment",
"parent_company",
"reporting_currency",
"company_info",
"company_logo",
"date_of_incorporation",
@@ -750,6 +751,7 @@
},
{
"default": "0",
"description": "Upon enabling this, the JV will be submitted for a different exchange rate.",
"fieldname": "submit_err_jv",
"fieldtype": "Check",
"label": "Submit ERR Journals?"
@@ -834,6 +836,14 @@
"fieldtype": "Select",
"label": "Reconciliation Takes Effect On",
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
},
{
"fieldname": "reporting_currency",
"fieldtype": "Link",
"label": "Reporting Currency",
"options": "Currency",
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-building",
@@ -841,7 +851,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-01-09 20:12:25.471544",
"modified": "2025-08-25 18:34:03.602046",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -903,9 +913,10 @@
"select": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -91,6 +91,7 @@ class Company(NestedSet):
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"
]
registration_details: DF.Code | None
reporting_currency: DF.Link | None
rgt: DF.Int
round_off_account: DF.Link | None
round_off_cost_center: DF.Link | None
@@ -153,6 +154,7 @@ class Company(NestedSet):
self.check_parent_changed()
self.set_chart_of_accounts()
self.validate_parent_company()
self.set_reporting_currency()
def validate_abbr(self):
if not self.abbr:
@@ -490,6 +492,14 @@ class Company(NestedSet):
if not is_group:
frappe.throw(_("Parent Company must be a group company"))
def set_reporting_currency(self):
self.reporting_currency = self.default_currency
if self.parent_company:
parent_reporting_currency = frappe.db.get_value(
"Company", self.parent_company, ["reporting_currency"]
)
self.reporting_currency = parent_reporting_currency
def set_default_accounts(self):
default_accounts = {
"default_cash_account": "Cash",
@@ -681,7 +691,7 @@ class Company(NestedSet):
frappe.db.sql("delete from tabBOM where company=%s", self.name)
for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"):
frappe.db.sql(
"delete from `tab{}` where parent in ({})" "".format(dt, ", ".join(["%s"] * len(boms))),
"delete from `tab{}` where parent in ({})".format(dt, ", ".join(["%s"] * len(boms))),
tuple(boms),
)

View File

@@ -814,7 +814,8 @@
"depends_on": "eval:doc.salary_mode == 'Bank'",
"fieldname": "iban",
"fieldtype": "Data",
"label": "IBAN"
"label": "IBAN",
"options": "IBAN"
}
],
"icon": "fa fa-user",
@@ -822,7 +823,7 @@
"image_field": "image",
"is_tree": 1,
"links": [],
"modified": "2025-07-04 08:29:34.347269",
"modified": "2025-08-29 11:52:12.819878",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
@@ -864,6 +865,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "employee_name",
"show_name_in_global_search": 1,
"sort_field": "creation",
@@ -871,4 +873,4 @@
"states": [],
"title_field": "employee_name",
"track_changes": 1
}
}

View File

@@ -4,9 +4,11 @@ import unittest
import frappe
import frappe.utils
from frappe.query_builder import Criterion
from frappe.tests import IntegrationTestCase
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
@@ -32,6 +34,32 @@ class TestEmployee(IntegrationTestCase):
employee_doc.save()
self.assertTrue("Employee" not in frappe.get_roles(user))
def test_employee_user_permission(self):
employee1 = make_employee("employee_1_test@company.com", create_user_permission=1)
employee2 = make_employee("employee_2_test@company.com", create_user_permission=1)
make_employee("employee_3_test@company.com", create_user_permission=1)
employee1_doc = frappe.get_doc("Employee", employee1)
employee2_doc = frappe.get_doc("Employee", employee2)
employee2_doc.reload()
employee2_doc.reports_to = employee1_doc.name
employee2_doc.save()
frappe.set_user(employee1_doc.user_id)
Employee = frappe.qb.DocType("Employee")
qb_employee_list = (
frappe.qb.from_(Employee)
.select(Employee.name)
.where(Criterion.all(build_qb_match_conditions("Employee")))
.orderby(Employee.Name)
).run(pluck=Employee.name)
employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name")
self.assertEqual(qb_employee_list, employee_list)
frappe.set_user("Administrator")
def tearDown(self):
frappe.db.rollback()

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