Compare commits

..

158 Commits

Author SHA1 Message Date
Frappe PR Bot
2863b0fe52 chore(release): Bumped to Version 15.87.1
## [15.87.1](https://github.com/frappe/erpnext/compare/v15.87.0...v15.87.1) (2025-11-07)

### Bug Fixes

* handle partial dn against reserved stock ([7466d91](7466d91e12))
2025-11-07 16:36:23 +00:00
rohitwaghchaure
6096b56a10 Merge pull request #50415 from frappe/mergify/bp/version-15/pr-50401
fix: handle partial dn against reserved stock (backport #50401)
2025-11-07 22:04:57 +05:30
Kavin
88fdab99a9 test: add test for partial dn against reserved stock
(cherry picked from commit ef719fe729)
2025-11-07 15:26:11 +00:00
Kavin
7466d91e12 fix: handle partial dn against reserved stock
(cherry picked from commit 9d979e34ab)
2025-11-07 15:26:10 +00:00
Frappe PR Bot
ad4fe2db9e chore(release): Bumped to Version 15.87.0
# [15.87.0](https://github.com/frappe/erpnext/compare/v15.86.0...v15.87.0) (2025-11-05)

### Features

* process period closing voucher ([8cdbaac](8cdbaacfff))
2025-11-05 12:37:51 +00:00
ruthra kumar
13de175b0f Merge pull request #50370 from frappe/mergify/bp/version-15/pr-50144
refactor: period closing voucher to handle large data volumes (backport #50144)
2025-11-05 18:06:29 +05:30
ruthra kumar
a333ec28e9 chore: resolve conflicts 2025-11-05 17:46:59 +05:30
ruthra kumar
e6d38d009f refactor: add paused to select option
(cherry picked from commit fca7abf4d6)
2025-11-05 12:15:04 +00:00
ruthra kumar
178eded259 refactor: minor changes on status
1. set to 'In Progress' on start of both legacy and new controller
2. force delete to avoid permission issues
3. default to 1hr timeout

(cherry picked from commit 9c13edc0b9)
2025-11-05 12:15:04 +00:00
ruthra kumar
5db7888a8d refactor: enable legacy controller by default for pcv
(cherry picked from commit fe39ce03bb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
2025-11-05 12:15:04 +00:00
ruthra kumar
83e0ce4020 chore: progress bar
(cherry picked from commit 0b88f98a86)
2025-11-05 12:15:03 +00:00
ruthra kumar
6f58614ef2 chore: remove scaffolding
(cherry picked from commit 191c0e65a1)
2025-11-05 12:15:03 +00:00
ruthra kumar
a3068c8bf6 refactor: abort processing of all tasks upon cancellation
(cherry picked from commit 090e155fd0)
2025-11-05 12:15:03 +00:00
ruthra kumar
bdeee94c18 refactor: more changes
1. 'Accounts Manager' has access to submit, cancel and delete
2. cancel and delete operation of PCV is linked with Proces PCV

(cherry picked from commit cae1237859)
2025-11-05 12:15:02 +00:00
ruthra kumar
db5013fca1 refactor: smaller methods
(cherry picked from commit fa3bd6f5a7)
2025-11-05 12:15:02 +00:00
ruthra kumar
2712310c0a refactor: utility to consolidate results from all dates
(cherry picked from commit 7406d83260)
2025-11-05 12:15:02 +00:00
ruthra kumar
a624ae07f0 refactor: utility to convert tuple key to str
(cherry picked from commit 5b464ae4c1)
2025-11-05 12:15:02 +00:00
ruthra kumar
347a7e3b1f refactor: cleanup and for better readability
(cherry picked from commit 653ae84b3e)
2025-11-05 12:15:01 +00:00
ruthra kumar
df4347a6cb refactor: make Accounts Closing Balance as well
(cherry picked from commit 6e32769e37)
2025-11-05 12:15:01 +00:00
ruthra kumar
7edbf25e60 refactor: store closing balance for Balnace sheet accounts
(cherry picked from commit 09e37bc98c)
2025-11-05 12:15:01 +00:00
ruthra kumar
7c85d7f163 refactor: calculate both balances from single queue
(cherry picked from commit 643e1fdce8)
2025-11-05 12:15:01 +00:00
ruthra kumar
6216547027 refactor: populate opening balances calculation table
(cherry picked from commit 86edacb781)
2025-11-05 12:15:00 +00:00
ruthra kumar
ac0a837bf3 chore: rename closing balance field
(cherry picked from commit cef879bb3b)
2025-11-05 12:15:00 +00:00
ruthra kumar
8be8a73a36 refactor: balances for both P&L and Balance sheet accounts
(cherry picked from commit 324bebfd44)
2025-11-05 12:15:00 +00:00
ruthra kumar
f49f60b5f4 refactor: maintain report type on each date
(cherry picked from commit 186d540502)
2025-11-05 12:15:00 +00:00
ruthra kumar
3a13c04a71 refactor: more stable pause and resume
(cherry picked from commit 9e93298f12)
2025-11-05 12:14:59 +00:00
ruthra kumar
62e8d4753e refactor: process on submit
(cherry picked from commit c738b6d356)
2025-11-05 12:14:59 +00:00
ruthra kumar
18c7121230 refactor: for better readability
(cherry picked from commit 8ba199016a)
2025-11-05 12:14:59 +00:00
ruthra kumar
37c1d665e3 refactor: store results as is and convert at the end
(cherry picked from commit f25ee3c53f)
2025-11-05 12:14:59 +00:00
ruthra kumar
0d9c711b6d refactor: build and post gl entries
(cherry picked from commit e88074ddec)
2025-11-05 12:14:58 +00:00
ruthra kumar
3b0a1bce3d refactor: store daily balances based on dimensions key
dimensions key is manually converted to string

(cherry picked from commit 1846de0d49)
2025-11-05 12:14:58 +00:00
ruthra kumar
efd90f554f refactor: store closing balance as JSON
(cherry picked from commit 1a31825409)
2025-11-05 12:14:58 +00:00
ruthra kumar
5992ce533e refactor: stable start, pause, resume and completion stages
(cherry picked from commit c839ebf593)
2025-11-05 12:14:58 +00:00
ruthra kumar
1c044759ac refactor: barebones functions
(cherry picked from commit 1c92b01542)
2025-11-05 12:14:57 +00:00
ruthra kumar
86a36b3ef4 refactor: temporarily save balances in JSON
(cherry picked from commit f44c908a8d)
2025-11-05 12:14:57 +00:00
ruthra kumar
cd830a6b6c refactor: child table in process pcv
(cherry picked from commit 0d09d21d2e)
2025-11-05 12:14:57 +00:00
ruthra kumar
dc1147d504 refactor: more data structure changes
(cherry picked from commit a15578f8f4)
2025-11-05 12:14:56 +00:00
ruthra kumar
797b418af4 refactor: checkbox for pcv controller
(cherry picked from commit 4888461be2)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
2025-11-05 12:14:56 +00:00
ruthra kumar
8cdbaacfff feat: process period closing voucher
(cherry picked from commit 7a93630629)
2025-11-05 12:14:56 +00:00
Frappe PR Bot
09d12f2f9f chore(release): Bumped to Version 15.86.0
# [15.86.0](https://github.com/frappe/erpnext/compare/v15.85.1...v15.86.0) (2025-11-04)

### Bug Fixes

* **accounts-receivable:** ensure report data with party account currency ([164333a](164333a730))
* **accounts-receivable:** ensure report data with party account currency (backport [#50035](https://github.com/frappe/erpnext/issues/50035)) ([#50311](https://github.com/frappe/erpnext/issues/50311)) ([ae23cdd](ae23cdd8e9))
* **accounts:** populate correct fields on GL Entry during discount accounting ([4076798](4076798707))
* added validation for default accounts on company ([d9d9230](d9d9230d4a))
* allow bulk edit for bill of material items ([afe42ee](afe42ee3e9))
* create GL entries via hooks only for submitted assets ([3d78277](3d7827731b))
* disallow material transfer if source and target warehouse are same ([#50331](https://github.com/frappe/erpnext/issues/50331)) ([d920520](d920520843)), closes [#48697](https://github.com/frappe/erpnext/issues/48697)
* handle None in last_valuation_rate check ([e3110b3](e3110b3993))
* job card timer ([053765a](053765a466))
* **pos:** order pos invoices by timestamp ([12903b1](12903b11ed))
* Respect allowed zero qty in SO and PO based on Buying/Selling settings when update items ([#49673](https://github.com/frappe/erpnext/issues/49673)) ([5d4da9c](5d4da9c68f))
* set valuation rate for rejected serial/batch item ([f72d14b](f72d14b1ca))

### Features

* option to exclude stand-alone returned sales invoices from the Gross Profit report ([017dc79](017dc792e6))

### Reverts

* Revert "refactor: add supplier filter in buying (backport [#50013](https://github.com/frappe/erpnext/issues/50013)) ([#50107](https://github.com/frappe/erpnext/issues/50107))" ([288570a](288570acde))
2025-11-04 12:32:53 +00:00
Diptanil Saha
19f66c6018 Merge pull request #50333 from frappe/version-15-hotfix
chore: release v15
2025-11-04 18:01:18 +05:30
rohitwaghchaure
de80215df5 Merge pull request #50338 from frappe/mergify/bp/version-15-hotfix/pr-50332
feat: option to exclude stand-alone returned sales invoices from the Gross Profit report (backport #50332)
2025-11-04 17:38:04 +05:30
Rohit Waghchaure
017dc792e6 feat: option to exclude stand-alone returned sales invoices from the Gross Profit report
(cherry picked from commit 52cf9d4950)
2025-11-04 11:30:46 +00:00
Mihir Kandoi
d920520843 fix: disallow material transfer if source and target warehouse are same (#50331)
* fix: disallow material transfer if source and target warehouse are same (#48697)

* fix: disallow material transfer if source and target warehouse are same

* fix: check on child row level instead of parent level

* chore: add docstring to function
2025-11-04 09:44:18 +00:00
mergify[bot]
ac901dee3c Add support for subcontract return from rejected warehouse (backport #50299) (#50330)
* Merge pull request #50299 from saadchaudharry/feature-subcontract-return-rejected

Add support for subcontract return from rejected warehouse

(cherry picked from commit 8854db51dd)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: Saad Chaudhary <47004596+saadchaudharry@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-11-04 09:09:41 +00:00
ruthra kumar
eed1abd272 Merge pull request #50313 from frappe/mergify/bp/version-15-hotfix/pr-49673
fix: Respect allowed zero qty in SO and PO based on Buying/Selling settings when update items (backport #49673)
2025-11-03 15:39:39 +05:30
ruthra kumar
ae23cdd8e9 fix(accounts-receivable): ensure report data with party account currency (backport #50035) (#50311)
* fix(accounts-receivable): ensure report data with party account currency

(cherry picked from commit 752ea7ee7d)

* test(accounts-receivable): add test case to validate report data with party account currency

(cherry picked from commit 3e4846ea3d)

---------

Co-authored-by: Bhavan23 <bhavan@aerele.in>
2025-11-03 15:30:24 +05:30
Kerolles Fathy
5d4da9c68f fix: Respect allowed zero qty in SO and PO based on Buying/Selling settings when update items (#49673)
* fix: respect allowed zero qty in SO and PO based on buying/selling settings

* refactor: remove parent_doctype param due to it's already avaliable

* refactor: rename is_allowed_zero_qty_for to is_allowed_zero_qty

* fix: prevent rate change for unit price items

* fix: unboundlocal variable rate_unchanged

* refactor: only throw on 'zero' qty items

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 44363c069e)
2025-11-03 09:47:30 +00:00
Khushi Rawat
990ada1656 Merge pull request #50312 from frappe/mergify/bp/version-15-hotfix/pr-50260
fix: create GL entries via hooks only for submitted assets (backport #50260)
2025-11-03 15:13:42 +05:30
Bhavan23
1e40fb00f5 test(accounts-receivable): add test case to validate report data with party account currency
(cherry picked from commit 3e4846ea3d)
2025-11-03 15:13:29 +05:30
khushi8112
3d7827731b fix: create GL entries via hooks only for submitted assets
(cherry picked from commit 33690975f6)
2025-11-03 09:21:58 +00:00
Bhavan23
164333a730 fix(accounts-receivable): ensure report data with party account currency
(cherry picked from commit 752ea7ee7d)
2025-11-03 09:12:44 +00:00
ruthra kumar
e1afcb39db Merge pull request #50309 from frappe/mergify/bp/version-15-hotfix/pr-50297
chore:there is no company field in bank (backport #50297)
2025-11-03 14:04:42 +05:30
Nihal Roshan
65e37756f6 refactor: remove company filter
'Bank' doctype has no 'company' field.

(cherry picked from commit f1682ea90e)
2025-11-03 08:31:56 +00:00
Diptanil Saha
c897220535 Merge pull request #50307 from frappe/mergify/bp/version-15-hotfix/pr-50305
fix(accounts): populate correct fields on GL Entry during discount accounting (backport #50305)
2025-11-03 13:58:28 +05:30
Diptanil Saha
b04c06e034 chore: resolve conflict 2025-11-03 13:22:33 +05:30
diptanilsaha
4076798707 fix(accounts): populate correct fields on GL Entry during discount accounting
(cherry picked from commit e41a7b8cd7)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2025-11-03 07:49:36 +00:00
rohitwaghchaure
8b8427cbf5 Merge pull request #50303 from frappe/mergify/bp/version-15-hotfix/pr-50300
fix: handle None in last_valuation_rate check (backport #50300)
2025-11-03 10:13:57 +05:30
Kavin
e3110b3993 fix: handle None in last_valuation_rate check
(cherry picked from commit a3058bf8cc)
2025-11-02 07:25:34 +00:00
Diptanil Saha
b5f8f2b6fa Merge pull request #50292 from frappe/mergify/bp/version-15-hotfix/pr-50291
fix: added validation for default accounts on company (backport #50291)
2025-11-01 10:04:59 +05:30
Diptanil Saha
1a3652bee1 chore: resolve conflict 2025-10-31 17:34:15 +05:30
Mihir Kandoi
253b945816 Merge pull request #50287 from frappe/revert-50107-mergify/bp/version-15-hotfix/pr-50013
Revert "refactor: add supplier filter in buying (backport #50013)"
2025-10-31 15:46:29 +05:30
diptanilsaha
d9d9230d4a fix: added validation for default accounts on company
(cherry picked from commit 4af1ae1470)

# Conflicts:
#	erpnext/setup/doctype/company/company.py
2025-10-31 10:07:42 +00:00
rohitwaghchaure
668ec641c1 Merge pull request #50290 from rohitwaghchaure/fixed-github-50262
fix: job card timer
2025-10-31 14:33:23 +05:30
Rohit Waghchaure
053765a466 fix: job card timer 2025-10-31 14:06:22 +05:30
Mihir Kandoi
288570acde Revert "refactor: add supplier filter in buying (backport #50013) (#50107)"
This reverts commit abb210bd18.
2025-10-31 13:59:58 +05:30
Frappe PR Bot
4ef07f97c9 chore(release): Bumped to Version 15.85.1
## [15.85.1](https://github.com/frappe/erpnext/compare/v15.85.0...v15.85.1) (2025-10-30)

### Bug Fixes

* **pos:** order pos invoices by timestamp ([8bd1e8f](8bd1e8ff74))
2025-10-30 15:42:14 +00:00
rohitwaghchaure
670579bde3 Merge pull request #50278 from frappe/mergify/bp/version-15/pr-50277
fix(pos): order pos invoices by timestamp (backport #50277)
2025-10-30 21:10:53 +05:30
Kavin
8bd1e8ff74 fix(pos): order pos invoices by timestamp
(cherry picked from commit 12903b11ed)
2025-10-30 13:20:53 +00:00
rohitwaghchaure
83f4970255 Merge pull request #50277 from aerele/support-51587
fix(pos): order pos invoices by timestamp
2025-10-30 18:49:44 +05:30
Kavin
12903b11ed fix(pos): order pos invoices by timestamp 2025-10-30 18:16:11 +05:30
rohitwaghchaure
7658076070 Merge pull request #50271 from frappe/mergify/bp/version-15-hotfix/pr-50270
fix: set valuation rate for rejected serial/batch item (backport #50270)
2025-10-30 09:27:57 +05:30
rohitwaghchaure
b019d2f45e chore: fix linters issue 2025-10-30 09:06:37 +05:30
venkat102
397bc96b40 test: add unit test for valuation rate on rejected serial/batch item
(cherry picked from commit d002959d35)
2025-10-30 03:32:00 +00:00
venkat102
f72d14b1ca fix: set valuation rate for rejected serial/batch item
(cherry picked from commit 614402cf6c)
2025-10-30 03:31:59 +00:00
rohitwaghchaure
0405d82b75 Merge pull request #50266 from frappe/mergify/bp/version-15-hotfix/pr-50264
fix: allow bulk edit for bill of material items (backport #50264)
2025-10-30 08:58:13 +05:30
Rohit Waghchaure
afe42ee3e9 fix: allow bulk edit for bill of material items
(cherry picked from commit 1f74e06791)
2025-10-29 12:51:38 +00:00
Frappe PR Bot
49f66a8a51 chore(release): Bumped to Version 15.85.0
# [15.85.0](https://github.com/frappe/erpnext/compare/v15.84.0...v15.85.0) (2025-10-28)

### Bug Fixes

* **accounts:** update payment mode account retrieval to use namespaced function ([2300660](23006601b2))
* add message for missing purchase orders in production plan ([5798409](5798409f69))
* **asset depreciations and balances:** showing opening entries ([81f19b9](81f19b950d))
* avoid group columns mutation ([158e1b2](158e1b28a9))
* **Bank Transaction:** make transaction ID non-unique ([b52b04a](b52b04a10c))
* fiscal year overlap validation for company-specific years ([482a796](482a796212))
* get valuation rate based of previous SLEs for material receipt ([e9f4a34](e9f4a34d8d))
* **gross profit:** remove customer name from columns ([a767253](a7672530f4))
* **journal-entry:** allow copy account currency when duplicating JV ([1b227b8](1b227b8b4f))
* optimized the slow query to get the batchwise available qty ([85bf936](85bf9366b0))
* Pass uom field name to update existing item qty ([93b2786](93b2786865))
* provision to find and fix incorrect serial and batch bundles ([2276741](22767410d5))
* recalculate amount based on allocated amount ([2a90bff](2a90bffb5f))
* resolve conflicts ([97147a4](97147a484d))
* sabb missed in the incorrect serial no valuation report ([8a995f2](8a995f28c9))
* set default value for as zero for additional asset cost ([ee5e4ec](ee5e4eccec))
* set status to Draft for auto-created assets from Purchase Receipt ([d8eddbf](d8eddbfd03))
* stock difference value for adjustment entry ([6c0694f](6c0694ff17))
* **Task:** make Timesheet-dependent fields no_copy (backport [#50130](https://github.com/frappe/erpnext/issues/50130)) ([#50196](https://github.com/frappe/erpnext/issues/50196)) ([4988ff8](4988ff84df))
* use correct field name ([a82fa8c](a82fa8c26b))

### Features

* Add posting date param for reverse GL entries ([1a7092d](1a7092d7b6))
* add project filter to Delayed Tasks Summary report ([82ca729](82ca729e2b))
2025-10-28 13:48:43 +00:00
Diptanil Saha
0a09bb8d17 Merge pull request #50249 from frappe/version-15-hotfix
chore: release v15
2025-10-28 19:17:10 +05:30
rohitwaghchaure
ba700bce90 Merge pull request #50252 from frappe/mergify/bp/version-15-hotfix/pr-50247
fix: provision to find and fix incorrect serial and batch bundles (backport #50247)
2025-10-28 18:21:46 +05:30
Rohit Waghchaure
22767410d5 fix: provision to find and fix incorrect serial and batch bundles
(cherry picked from commit 10ad56060c)
2025-10-28 12:15:11 +00:00
Diptanil Saha
89e7a260fd Merge pull request #50251 from frappe/mergify/bp/version-15-hotfix/pr-50137
fix(payment-reco): recalculate amount based on allocated amount (backport #50137)
2025-10-28 17:34:10 +05:30
l0gesh29
2a90bffb5f fix: recalculate amount based on allocated amount
(cherry picked from commit 5a7a6a9bd5)
2025-10-28 11:58:10 +00:00
Diptanil Saha
7434522ea1 Merge pull request #50250 from frappe/mergify/bp/version-15-hotfix/pr-50220
fix(gross profit): remove customer name from columns (backport #50220)
2025-10-28 15:37:53 +05:30
ravibharathi656
158e1b28a9 fix: avoid group columns mutation
(cherry picked from commit 7ce81127d2)
2025-10-28 09:51:23 +00:00
ravibharathi656
a7672530f4 fix(gross profit): remove customer name from columns
(cherry picked from commit 0009925af0)
2025-10-28 09:51:23 +00:00
Diptanil Saha
ad33735541 Merge pull request #50245 from frappe/mergify/bp/version-15-hotfix/pr-50213
fix(Bank Transaction)!: make transaction ID non-unique (backport #50213)
2025-10-28 10:20:49 +05:30
Diptanil Saha
977f385006 chore: resolve conflict 2025-10-28 09:07:14 +05:30
barredterra
b52b04a10c fix(Bank Transaction): make transaction ID non-unique
(cherry picked from commit a2a41a0eaa)

# Conflicts:
#	erpnext/accounts/doctype/bank_transaction/bank_transaction.json
2025-10-28 02:45:52 +00:00
Diptanil Saha
05b89b4121 Merge pull request #50243 from frappe/mergify/bp/version-15-hotfix/pr-49599
fix(accounts): reference get_payment_mode_account correctly in Sales Invoice (backport #49599)
2025-10-28 08:08:36 +05:30
Diptanil Saha
36983c97cb chore: resolve conflict 2025-10-28 08:06:10 +05:30
KerollesFathy
23006601b2 fix(accounts): update payment mode account retrieval to use namespaced function
(cherry picked from commit 0af74aef00)

# Conflicts:
#	erpnext/public/js/controllers/accounts.js
2025-10-27 16:50:10 +00:00
rohitwaghchaure
37e9461ece Merge pull request #50239 from frappe/mergify/bp/version-15-hotfix/pr-50233
fix: optimized the slow query to get the batch-wise available qty (backport #50233)
2025-10-27 17:31:15 +05:30
Mihir Kandoi
126d79dcd2 Merge pull request #50241 from frappe/mergify/bp/version-15-hotfix/pr-50221
fix: Pass uom field name to update existing item qty (backport #50221)
2025-10-27 17:29:29 +05:30
Kavin
93b2786865 fix: Pass uom field name to update existing item qty
(cherry picked from commit 23d69389ec)
2025-10-27 11:48:44 +00:00
rohitwaghchaure
2680f85430 chore: fix linters issue 2025-10-27 17:06:45 +05:30
rohitwaghchaure
2d7fde024d chore: fix conflicts 2025-10-27 16:57:00 +05:30
Rohit Waghchaure
85bf9366b0 fix: optimized the slow query to get the batchwise available qty
(cherry picked from commit 9c21567309)

# Conflicts:
#	erpnext/stock/serial_batch_bundle.py
2025-10-27 11:19:24 +00:00
Khushi Rawat
86f507bb6e Merge pull request #50238 from frappe/mergify/bp/version-15-hotfix/pr-50222
fix(asset depreciations and balances): showing opening entries (backport #50222)
2025-10-27 16:35:46 +05:30
ravibharathi656
81f19b950d fix(asset depreciations and balances): showing opening entries
(cherry picked from commit d3afa67be3)
2025-10-27 10:48:36 +00:00
Mihir Kandoi
7b4eac2294 Merge pull request #50236 from Abdeali099/fix-msg
fix: add message for missing purchase orders in production plan
2025-10-27 15:54:09 +05:30
Diptanil Saha
aff4b62258 Merge pull request #50234 from frappe/mergify/bp/version-15-hotfix/pr-50039
fix(journal-entry): allow copy account currency when duplicating JV (backport #50039)
2025-10-27 15:36:40 +05:30
Abdeali Chharchhoda
5798409f69 fix: add message for missing purchase orders in production plan 2025-10-27 15:09:22 +05:30
Bhavan23
1b227b8b4f fix(journal-entry): allow copy account currency when duplicating JV
(cherry picked from commit 76748e4573)
2025-10-27 08:45:01 +00:00
Diptanil Saha
4eca7f0524 Merge pull request #50230 from frappe/mergify/bp/version-15-hotfix/pr-50103
fix: fiscal year overlap validation for company-specific years (backport #50103)
2025-10-27 11:01:57 +05:30
rehansari26
4bb66179ab test: replace get_doc with new_doc in fiscal year tests
(cherry picked from commit d59e55fb08)
2025-10-27 05:08:36 +00:00
Rehan Ansari
d84a29e1a1 test: fiscal year overlap validation for company-specific years
(cherry picked from commit 94ae098854)
2025-10-27 05:08:36 +00:00
Rehan Ansari
482a796212 fix: fiscal year overlap validation for company-specific years
(cherry picked from commit 57aaf34d3e)
2025-10-27 05:08:36 +00:00
Diptanil Saha
41694d3751 Merge pull request #50223 from frappe/mergify/bp/version-15-hotfix/pr-49889
feat: add project filter to Delayed Tasks Summary report (backport #49889)
2025-10-24 22:27:54 +05:30
Rehan Ansari
82ca729e2b feat: add project filter to Delayed Tasks Summary report
(cherry picked from commit 88097e78d2)
2025-10-24 16:42:15 +00:00
rohitwaghchaure
e67361257e Merge pull request #50216 from frappe/mergify/bp/version-15-hotfix/pr-50215
fix: get valuation rate based of previous SLEs for material receipt (backport #50215)
2025-10-24 00:05:12 +05:30
Rohit Waghchaure
e9f4a34d8d fix: get valuation rate based of previous SLEs for material receipt
(cherry picked from commit fa9ef6708f)
2025-10-23 18:30:20 +00:00
Khushi Rawat
c6c4e31b32 Merge pull request #50211 from frappe/mergify/bp/version-15-hotfix/pr-50209
fix: set status to Draft for auto-created assets from Purchase Receipt (backport #50209)
2025-10-23 23:33:42 +05:30
khushi8112
ee5e4eccec fix: set default value for as zero for additional asset cost 2025-10-23 22:51:30 +05:30
khushi8112
a82fa8c26b fix: use correct field name 2025-10-23 20:45:08 +05:30
khushi8112
97147a484d fix: resolve conflicts 2025-10-23 19:09:30 +05:30
khushi8112
d8eddbfd03 fix: set status to Draft for auto-created assets from Purchase Receipt
(cherry picked from commit 20c2cb40d1)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py
#	erpnext/patches.txt
2025-10-23 13:03:17 +00:00
Deepesh Garg
977464e6a7 Merge pull request #50206 from frappe/mergify/bp/version-15-hotfix/pr-50204
feat: Add posting date param for reverse GL entries (#50204)
2025-10-23 16:02:07 +05:30
Deepesh Garg
db84df0cbd chore: Linting issues
(cherry picked from commit 05e1a737f1)
2025-10-23 08:12:31 +00:00
Deepesh Garg
1a7092d7b6 feat: Add posting date param for reverse GL entries
(cherry picked from commit 38988bf797)
2025-10-23 08:12:30 +00:00
rohitwaghchaure
fcd05bb7e4 Merge pull request #50202 from frappe/mergify/bp/version-15-hotfix/pr-50200
fix: stock difference value for adjustment entry (backport #50200)
2025-10-23 10:31:00 +05:30
Rohit Waghchaure
6c0694ff17 fix: stock difference value for adjustment entry
(cherry picked from commit fb4c7de86c)
2025-10-22 17:03:12 +00:00
mergify[bot]
4988ff84df fix(Task): make Timesheet-dependent fields no_copy (backport #50130) (#50196)
* fix(Task): make Timesheet-dependent fields no_copy

(cherry picked from commit 2a2e4b5423)

# Conflicts:
#	erpnext/projects/doctype/task/task.json

* chore: resolve conflicts

---------

Co-authored-by: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-10-22 08:34:11 +00:00
Mihir Kandoi
e5d0116924 Merge pull request #50194 from frappe/mergify/bp/version-15-hotfix/pr-50151
chore: remove print statement (backport #50151)
2025-10-22 14:01:04 +05:30
Mihir Kandoi
38d3b17dfe Merge pull request #50195 from frappe/mergify/bp/version-15-hotfix/pr-50153
fix: Address title error when create new transporter on driver  (backport #50153)
2025-10-22 13:48:31 +05:30
Kerolles Fathy
89d305ac9f Merge pull request #50153 from KerollesFathy/fix-address-error-when-create-transporter
fix: Address title error when create new transporter on driver
(cherry picked from commit 8efcf6cb38)
2025-10-22 08:14:25 +00:00
Abdeali Chharchhoda
8b2be43897 chore: remove print statement
(cherry picked from commit 1ad4dc9066)
2025-10-22 08:14:24 +00:00
rohitwaghchaure
0bd3b5aac2 Merge pull request #50192 from frappe/mergify/bp/version-15-hotfix/pr-50191
fix: sabb missed in the incorrect serial no valuation report (backport #50191)
2025-10-22 12:17:53 +05:30
Rohit Waghchaure
8a995f28c9 fix: sabb missed in the incorrect serial no valuation report
(cherry picked from commit b50bac6788)
2025-10-22 06:26:01 +00:00
Frappe PR Bot
e714b82802 chore(release): Bumped to Version 15.84.0
# [15.84.0](https://github.com/frappe/erpnext/compare/v15.83.2...v15.84.0) (2025-10-21)

### Bug Fixes

* added exception handling on service level agreement apply hook ([#50096](https://github.com/frappe/erpnext/issues/50096)) ([a3a6d39](a3a6d39436))
* adjustment entry ([76cfa28](76cfa28a42))
* correct monthly sales history (backport [#50056](https://github.com/frappe/erpnext/issues/50056)) ([#50179](https://github.com/frappe/erpnext/issues/50179)) ([afc2d95](afc2d95736))
* handle flt conversion for prev_ordered_qty ([3d3e116](3d3e116797))
* internal transfer entry with serial/batch ([d67a439](d67a439051))
* **point-of-sale:** render payment methods only payment component is visible ([2b0281c](2b0281c510))
* preview stock ledger for manual serial and batch values ([c64dcf3](c64dcf3423))
* **stock:** remove duplicate fields ([58a1383](58a1383380))
* validation for negative batch ([b9dd05f](b9dd05f292))

### Features

* set options for IBAN fields (backport [#49377](https://github.com/frappe/erpnext/issues/49377)) ([#49413](https://github.com/frappe/erpnext/issues/49413)) ([bd3a132](bd3a132868))

### Performance Improvements

* Add index for faster queries ([#50175](https://github.com/frappe/erpnext/issues/50175)) ([0571ed0](0571ed0735))
2025-10-21 12:44:58 +00:00
Diptanil Saha
7ec6ef3139 Merge pull request #50182 from frappe/version-15-hotfix
chore: release v15
2025-10-21 18:13:27 +05:30
mergify[bot]
afc2d95736 fix: correct monthly sales history (backport #50056) (#50179)
fix: correct monthly sales history (#50056)

* fix: correct monthly sales history

* fix: correct monthly sales history calculation

* chore: remove unrelated file accidentally committed

---------


(cherry picked from commit f51ed30c23)

Co-authored-by: Ahmed AbuKhatwa <82771130+AhmedAbokhatwa@users.noreply.github.com>
Co-authored-by: AhmedAbukhatwa <Ahmedabukhatwa1@gmail.com>
2025-10-21 11:06:02 +05:30
Deepesh Garg
1e9ffe351b Merge pull request #50178 from frappe/mergify/bp/version-15-hotfix/pr-50175
perf: Add index for faster queries
2025-10-21 09:56:35 +05:30
Deepesh Garg
0571ed0735 perf: Add index for faster queries (#50175)
(cherry picked from commit 7a91ec3e33)
2025-10-20 18:43:48 +00:00
mergify[bot]
43941ecd5e refactor: simplify expression (backport #50168) (#50170)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-10-19 23:59:27 +02:00
Frappe PR Bot
8b9788ca74 chore(release): Bumped to Version 15.83.2
## [15.83.2](https://github.com/frappe/erpnext/compare/v15.83.1...v15.83.2) (2025-10-18)

### Bug Fixes

* internal transfer entry with serial/batch ([97cdac1](97cdac10d7))
* **stock:** remove duplicate fields ([5e8e6ef](5e8e6ef2f3))
2025-10-18 04:42:12 +00:00
rohitwaghchaure
eb38bcdab7 Merge pull request #50164 from frappe/mergify/bp/version-15/pr-50158
fix: internal transfer entry with serial/batch (backport #50156) (backport #50158)
2025-10-18 10:10:35 +05:30
rohitwaghchaure
9876d1c7e6 Merge pull request #50165 from frappe/mergify/bp/version-15/pr-50157
fix(stock-settings): remove duplicate fields (backport #50157)
2025-10-18 10:10:20 +05:30
Kavin
5e8e6ef2f3 fix(stock): remove duplicate fields
(cherry picked from commit 58a1383380)
2025-10-18 03:49:50 +00:00
rohitwaghchaure
2055c4e8d3 Merge pull request #50157 from aerele/support-51015
fix(stock-settings): remove duplicate fields
2025-10-18 09:19:21 +05:30
Rohit Waghchaure
97cdac10d7 fix: internal transfer entry with serial/batch
(cherry picked from commit 9b4e62a758)
(cherry picked from commit d67a439051)
2025-10-18 03:47:47 +00:00
rohitwaghchaure
df22902035 Merge pull request #50158 from frappe/mergify/bp/version-15-hotfix/pr-50156
fix: internal transfer entry with serial/batch (backport #50156)
2025-10-17 21:32:16 +05:30
Rohit Waghchaure
d67a439051 fix: internal transfer entry with serial/batch
(cherry picked from commit 9b4e62a758)
2025-10-17 15:34:30 +00:00
Kavin
58a1383380 fix(stock): remove duplicate fields 2025-10-17 18:35:53 +05:30
rohitwaghchaure
f5af5dd8c0 Merge pull request #50145 from frappe/mergify/bp/version-15-hotfix/pr-50123
fix: validation for negative batch (backport #50123)
2025-10-17 12:34:15 +05:30
Rohit Waghchaure
b9dd05f292 fix: validation for negative batch
(cherry picked from commit f9c8f27586)
2025-10-17 06:07:37 +00:00
Diptanil Saha
44f39e0b43 Merge pull request #50142 from frappe/mergify/bp/version-15-hotfix/pr-50141
fix(point-of-sale): render payment methods only when payment component is visible (backport #50141)
2025-10-17 01:46:05 +05:30
diptanilsaha
2b0281c510 fix(point-of-sale): render payment methods only payment component is visible
(cherry picked from commit 7dc4306640)
2025-10-16 20:13:33 +00:00
rohitwaghchaure
03766e8980 Merge pull request #50117 from frappe/mergify/bp/version-15-hotfix/pr-50113
fix: adjustment entry (backport #50113)
2025-10-15 21:31:32 +05:30
Diptanil Saha
e7b7377b7e Merge pull request #50120 from frappe/mergify/bp/version-15-hotfix/pr-50096
fix: added exception handling on service level agreement apply hook (backport #50096)
2025-10-15 19:51:23 +05:30
Diptanil Saha
a3a6d39436 fix: added exception handling on service level agreement apply hook (#50096)
* fix: added exception handling on service level agreement apply hook

* Revert "fix: added exception handling on service level agreement apply hook"

This reverts commit dae93aa96f.

* fix: Ignore missing SLA table during install/uninstall

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
(cherry picked from commit 182c9fd966)
2025-10-15 14:04:17 +00:00
Rohit Waghchaure
76cfa28a42 fix: adjustment entry
(cherry picked from commit c0851abaee)
2025-10-15 10:30:33 +00:00
Diptanil Saha
afa7d2b595 Merge pull request #50109 from frappe/mergify/bp/version-15-hotfix/pr-50095
fix: preview stock ledger for manual serial and batch values (backport #50095)
2025-10-15 13:14:13 +05:30
Kavin
c64dcf3423 fix: preview stock ledger for manual serial and batch values
(cherry picked from commit c5f68d0b27)
2025-10-15 05:47:41 +00:00
Mihir Kandoi
3fa38d0b4e Merge pull request #50106 from frappe/mergify/bp/version-15-hotfix/pr-50078
fix: handle flt conversion for prev_ordered_qty (backport #50078)
2025-10-15 00:02:09 +05:30
mergify[bot]
abb210bd18 refactor: add supplier filter in buying (backport #50013) (#50107)
* refactor: add supplier filter in buying

(cherry picked from commit 108b108d64)

# Conflicts:
#	erpnext/public/js/controllers/buying.js

* chore: resolve conflicts

---------

Co-authored-by: manikandan-s-18 <manikandansundar18@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-10-14 18:22:20 +00:00
Kavin
3d3e116797 fix: handle flt conversion for prev_ordered_qty
(cherry picked from commit 77c35ef47f)
2025-10-14 18:10:30 +00:00
mergify[bot]
bd3a132868 feat: set options for IBAN fields (backport #49377) (#49413)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-10-14 14:42:08 +00:00
77 changed files with 2023 additions and 228 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.83.1"
__version__ = "15.87.1"
def get_default_company(user=None):

View File

@@ -110,6 +110,7 @@ class Account(NestedSet):
self.validate_parent_child_account_type()
self.validate_root_details()
self.validate_account_number()
self.validate_disabled()
self.validate_group_or_ledger()
self.set_root_and_report_type()
self.validate_mandatory()
@@ -254,6 +255,14 @@ class Account(NestedSet):
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_disabled(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.disabled) == cint(self.disabled):
return
if cint(self.disabled):
self.validate_default_accounts_in_company()
def validate_group_or_ledger(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
@@ -264,9 +273,32 @@ class Account(NestedSet):
elif cint(self.is_group):
if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
self.validate_default_accounts_in_company()
elif self.check_if_child_exists():
throw(_("Account with child nodes cannot be set as ledger"))
def validate_default_accounts_in_company(self):
default_account_fields = get_company_default_account_fields()
company_default_accounts = frappe.db.get_value(
"Company", self.company, list(default_account_fields.keys()), as_dict=1
)
msg = _("Account {0} cannot be disabled as it is already set as {1} for {2}.")
if not self.disabled:
msg = _("Account {0} cannot be converted to Group as it is already set as {1} for {2}.")
for d in default_account_fields:
if company_default_accounts.get(d) == self.name:
throw(
msg.format(
frappe.bold(self.name),
frappe.bold(default_account_fields.get(d)),
frappe.bold(self.company),
)
)
def validate_frozen_accounts_modifier(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
@@ -627,3 +659,27 @@ def _ensure_idle_system():
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)
def get_company_default_account_fields():
return {
"default_bank_account": "Default Bank Account",
"default_cash_account": "Default Cash Account",
"default_receivable_account": "Default Receivable Account",
"default_payable_account": "Default Payable Account",
"default_expense_account": "Default Expense Account",
"default_income_account": "Default Income Account",
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
"stock_adjustment_account": "Stock Adjustment Account",
"write_off_account": "Write Off Account",
"default_discount_account": "Default Payment Discount Account",
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
"round_off_account": "Round Off Account",
"default_deferred_revenue_account": "Default Deferred Revenue Account",
"default_deferred_expense_account": "Default Deferred Expense Account",
"accumulated_depreciation_account": "Accumulated Depreciation Account",
"depreciation_expense_account": "Depreciation Expense Account",
"disposal_account": "Gain/Loss Account on Asset Disposal",
}

View File

@@ -75,6 +75,7 @@
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
@@ -629,6 +630,12 @@
"fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
},
{
"default": "1",
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
}
],
"icon": "icon-cog",
@@ -636,7 +643,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-07-18 13:56:47.192437",
"modified": "2025-10-20 14:06:08.870427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -73,6 +73,7 @@ class AccountsSettings(Document):
submit_journal_entries: DF.Check
unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check
use_legacy_controller_for_pcv: DF.Check
# end: auto-generated types
def validate(self):

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,6 +284,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "bank,account",
"sort_field": "modified",
"sort_order": "DESC",

View File

@@ -9,7 +9,8 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
from frappe.utils import comma_and, get_link_to_form
from frappe.utils import comma_and, get_link_to_form, validate_iban
from frappe.utils.deprecations import deprecated
class BankAccount(Document):
@@ -52,7 +53,6 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
self.validate_account()
self.update_default_bank_account()
@@ -72,34 +72,10 @@ class BankAccount(Document):
if self.is_company_account and not self.company:
frappe.throw(_("Company is manadatory for company account"))
@deprecated
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"))
"""Kept for backward compatibility, will be removed in v16."""
validate_iban(self.iban, throw=True)
def update_default_bank_account(self):
if self.is_default and not self.disabled:

View File

@@ -3,45 +3,8 @@
import unittest
import frappe
from frappe import ValidationError
# test_records = frappe.get_test_records('Bank Account')
class TestBankAccount(unittest.TestCase):
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

@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("bank", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -146,6 +146,7 @@
"fieldname": "iban",
"fieldtype": "Data",
"label": "IBAN",
"options": "IBAN",
"read_only": 1
},
{
@@ -216,7 +217,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2025-09-25 23:44:12.473583",
"modified": "2025-09-26 00:38:17.584694",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",

View File

@@ -123,8 +123,7 @@
"fieldname": "transaction_id",
"fieldtype": "Data",
"label": "Transaction ID",
"read_only": 1,
"unique": 1
"read_only": 1
},
{
"allow_on_submit": 1,
@@ -223,7 +222,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 +238,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-09-26 17:06:29.207673",
"modified": "2025-10-23 17:32:58.514807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -99,7 +99,7 @@ class FiscalYear(Document):
)
overlap = False
if not self.get("companies") or not company_for_existing:
if not self.get("companies") and not company_for_existing:
overlap = True
for d in self.get("companies"):

View File

@@ -26,6 +26,27 @@ class TestFiscalYear(unittest.TestCase):
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
def test_company_fiscal_year_overlap(self):
for name in ["_Test Global FY 2001", "_Test Company FY 2001"]:
if frappe.db.exists("Fiscal Year", name):
frappe.delete_doc("Fiscal Year", name)
global_fy = frappe.new_doc("Fiscal Year")
global_fy.year = "_Test Global FY 2001"
global_fy.year_start_date = "2001-04-01"
global_fy.year_end_date = "2002-03-31"
global_fy.insert()
company_fy = frappe.new_doc("Fiscal Year")
company_fy.year = "_Test Company FY 2001"
company_fy.year_start_date = "2001-01-01"
company_fy.year_end_date = "2001-12-31"
company_fy.append("companies", {"company": "_Test Company"})
company_fy.insert()
self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name))
self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name))
def test_record_generator():
test_records = [

View File

@@ -106,7 +106,6 @@
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"no_copy": 1,
"options": "Currency",
"print_hide": 1,
"read_only": 1
@@ -272,7 +271,8 @@
"label": "Advance Voucher Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "advance_voucher_no",
@@ -280,13 +280,14 @@
"label": "Advance Voucher No",
"no_copy": 1,
"options": "advance_voucher_type",
"read_only": 1
"read_only": 1,
"search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-09-29 13:01:48.916517",
"modified": "2025-10-27 13:48:32.805100",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -385,6 +385,16 @@ frappe.ui.form.on("Payment Reconciliation Allocation", {
// filter payment
let payment = frm.doc.payments.filter((x) => x.reference_name == row.reference_name);
let amount = payment[0].amount;
for (const d of frm.doc.allocation) {
if (row.reference_name == d.reference_name && amount) {
if (d.allocated_amount <= amount) {
d.amount = amount;
amount -= d.allocated_amount;
}
}
}
frm.call({
doc: frm.doc,
method: "calculate_difference_on_allocation_change",

View File

@@ -226,7 +226,8 @@
"fetch_from": "bank_account.iban",
"fieldname": "iban",
"fieldtype": "Read Only",
"label": "IBAN"
"label": "IBAN",
"options": "IBAN"
},
{
"fetch_from": "bank_account.branch_code",
@@ -443,11 +444,12 @@
"label": "Phone Number"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-12-27 21:29:10.361894",
"modified": "2025-08-29 11:52:48.555415",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -482,8 +484,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_preview_popup": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -4,6 +4,8 @@
frappe.ui.form.on("Period Closing Voucher", {
onload: function (frm) {
if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date());
frm.ignore_doctypes_on_cancel_all = ["Process Period Closing Voucher"];
},
setup: function (frm) {

View File

@@ -132,7 +132,11 @@ class PeriodClosingVoucher(AccountsController):
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
self.make_gl_entries()
else:
ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name})
ppcv.save().submit()
def on_cancel(self):
self.ignore_linked_doctypes = (
@@ -140,11 +144,29 @@ class PeriodClosingVoucher(AccountsController):
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
"Process Period Closing Voucher",
)
self.block_if_future_closing_voucher_exists()
if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
self.cancel_process_pcv_docs()
self.db_set("gle_processing_status", "In Progress")
self.cancel_gl_entries()
def cancel_process_pcv_docs(self):
ppcvs = frappe.db.get_all("Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": 1})
for x in ppcvs:
frappe.get_doc("Process Period Closing Voucher", x.name).cancel()
def on_trash(self):
super().on_trash()
ppcvs = frappe.db.get_all(
"Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": ["in", [1, 2]]}
)
for x in ppcvs:
frappe.delete_doc("Process Period Closing Voucher", x.name, force=True, ignore_permissions=True)
def make_gl_entries(self):
if frappe.db.estimate_count("GL Entry") > 100_000:
frappe.enqueue(

View File

@@ -14,6 +14,10 @@ from erpnext.accounts.utils import get_fiscal_year
class TestPeriodClosingVoucher(unittest.TestCase):
def setUp(self):
super().setUp()
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")

View File

@@ -161,6 +161,8 @@ def get_pos_invoices(start, end, pos_profile, user):
`tabPOS Invoice`
where
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
order by
timestamp
""",
(user, pos_profile),
as_dict=1,

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Period Closing Voucher", {
refresh(frm) {
if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Start");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.start_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("Job Started"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus == 1 && ["Running"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Pause");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.pause_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("PCV Paused"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus == 1 && ["Paused"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Resume");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.resume_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("PCV Resumed"));
frm.reload_doc();
}
});
});
}
// progress bar
let progress = 0;
let normal_finished = frm.doc.normal_balances.filter((x) => x.status == "Completed").length;
let opening_finished = frm.doc.z_opening_balances.filter((x) => x.status == "Completed").length;
progress =
((normal_finished + opening_finished) /
(frm.doc.normal_balances.length + frm.doc.z_opening_balances.length)) *
100;
frm.dashboard.add_progress("Books closure progress", progress, "");
},
});

View File

@@ -0,0 +1,113 @@
{
"actions": [],
"autoname": "format:Process-PCV-{###}",
"creation": "2025-09-25 15:44:03.534699",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"parent_pcv",
"status",
"p_l_closing_balance",
"normal_balances",
"bs_closing_balance",
"z_opening_balances",
"amended_from"
],
"fields": [
{
"fieldname": "parent_pcv",
"fieldtype": "Link",
"in_list_view": 1,
"label": "PCV",
"options": "Period Closing Voucher",
"reqd": 1
},
{
"default": "Queued",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Process Period Closing Voucher",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "p_l_closing_balance",
"fieldtype": "JSON",
"label": "P&L Closing Balance",
"no_copy": 1
},
{
"fieldname": "normal_balances",
"fieldtype": "Table",
"label": "Dates to Process",
"no_copy": 1,
"options": "Process Period Closing Voucher Detail"
},
{
"fieldname": "z_opening_balances",
"fieldtype": "Table",
"label": "Opening Balances",
"no_copy": 1,
"options": "Process Period Closing Voucher Detail"
},
{
"fieldname": "bs_closing_balance",
"fieldtype": "JSON",
"label": "Balance Sheet Closing Balance"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-05 11:40:24.996403",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,559 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
from datetime import timedelta
import frappe
from frappe import qb
from frappe.model.document import Document
from frappe.query_builder.functions import Count, Max, Min, Sum
from frappe.utils import add_days, flt, get_datetime
from frappe.utils.scheduler import is_scheduler_inactive
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
class ProcessPeriodClosingVoucher(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.process_period_closing_voucher_detail.process_period_closing_voucher_detail import (
ProcessPeriodClosingVoucherDetail,
)
amended_from: DF.Link | None
bs_closing_balance: DF.JSON | None
normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
p_l_closing_balance: DF.JSON | None
parent_pcv: DF.Link
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
# end: auto-generated types
def validate(self):
self.status = "Queued"
self.populate_processing_tables()
def populate_processing_tables(self):
self.generate_pcv_dates()
self.generate_opening_balances_dates()
def get_dates(self, start, end):
return [start + timedelta(days=x) for x in range((end - start).days + 1)]
def generate_pcv_dates(self):
self.normal_balances = []
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
dates = self.get_dates(get_datetime(pcv.period_start_date), get_datetime(pcv.period_end_date))
for x in dates:
self.append(
"normal_balances",
{"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"},
)
self.append(
"normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}
)
def generate_opening_balances_dates(self):
self.z_opening_balances = []
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
if pcv.is_first_period_closing_voucher():
gl = qb.DocType("GL Entry")
min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
dates = self.get_dates(get_datetime(min), get_datetime(max))
for x in dates:
self.append(
"z_opening_balances",
{"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"},
)
def on_submit(self):
start_pcv_processing(self.name)
def on_cancel(self):
cancel_pcv_processing(self.name)
@frappe.whitelist()
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
if normal_balances := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=4,
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": x.processing_date,
"parent": docname,
"report_type": x.report_type,
"parentfield": x.parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist()
def pause_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist()
def cancel_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist()
def resume_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()
if paused_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Paused"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
start_pcv_processing(docname)
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
for i, dimension in enumerate(dimension_fields):
gl_entry[dimension] = dimension_values[i]
def get_gle_for_pl_account(pcv, acc, balances, dimensions):
balance_in_account_currency = flt(balances.debit_in_account_currency) - flt(
balances.credit_in_account_currency
)
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
gl_entry = frappe._dict(
{
"company": pcv.company,
"posting_date": pcv.period_end_date,
"account": acc,
"account_currency": balances.account_currency,
"debit_in_account_currency": abs(balance_in_account_currency)
if balance_in_account_currency < 0
else 0,
"debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
"credit_in_account_currency": abs(balance_in_account_currency)
if balance_in_account_currency > 0
else 0,
"credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0,
"is_period_closing_voucher_entry": 1,
"voucher_type": "Period Closing Voucher",
"voucher_no": pcv.name,
"fiscal_year": pcv.fiscal_year,
"remarks": pcv.remarks,
"is_opening": "No",
}
)
# update dimensions
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
return gl_entry
def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
gl_entry = frappe._dict(
{
"company": pcv.company,
"posting_date": pcv.period_end_date,
"account": pcv.closing_account_head,
"account_currency": frappe.db.get_value("Account", pcv.closing_account_head, "account_currency"),
"debit_in_account_currency": debit,
"debit": debit,
"credit_in_account_currency": credit,
"credit": credit,
"is_period_closing_voucher_entry": 1,
"voucher_type": "Period Closing Voucher",
"voucher_no": pcv.name,
"fiscal_year": pcv.fiscal_year,
"remarks": pcv.remarks,
"is_opening": "No",
}
)
# update dimensions
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
return gl_entry
@frappe.whitelist()
def schedule_next_date(docname: str):
if to_process := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=1,
):
if not is_scheduler_inactive():
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": to_process[0].processing_date,
"parent": docname,
"report_type": to_process[0].report_type,
"parentfield": to_process[0].parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=to_process[0].processing_date,
report_type=to_process[0].report_type,
parentfield=to_process[0].parentfield,
)
else:
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
total_no_of_dates = (
qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0]
)
completed = (
qb.from_(ppcvd)
.select(Count(ppcvd.star))
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Completed"))
.run()[0][0]
)
# Ensure both normal and opening balances are processed for all dates
if total_no_of_dates == completed:
summarize_and_post_ledger_entries(docname)
def make_dict_json_compliant(dimension_wise_balance) -> dict:
"""
convert tuple -> str
JSON doesn't support dictionary with tuple keys
"""
converted_dict = {}
for k, v in dimension_wise_balance.items():
str_key = [str(x) for x in k]
str_key = ",".join(str_key)
converted_dict[str_key] = v
return converted_dict
def get_consolidated_gles(balances, report_type) -> list:
gl_entries = []
for x in balances:
if x.report_type == report_type:
closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)]
gl_entries.extend(closing_balances)
return gl_entries
def get_gl_entries(docname):
"""
Calculate total closing balance of all P&L accounts across PCV start and end date
"""
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
# calculate balance
gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss")
pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
# save
json_dict = make_dict_json_compliant(pl_dimension_wise_acc_balance)
frappe.db.set_value(
"Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict)
)
# build gl map
pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv)
pl_accounts_reverse_gle = []
closing_account_gle = []
for dimensions, account_balances in pl_dimension_wise_acc_balance.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if balance_in_company_currency:
pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions))
closing_account_gle.append(get_gle_for_closing_account(pcv, account_balances["balances"], dimensions))
return pl_accounts_reverse_gle, closing_account_gle
def calculate_balance_sheet_balance(docname):
"""
Calculate total closing balance of all P&L accounts across PCV start and end date.
If it is first PCV, opening entries are also considered
"""
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
gl_entries = get_consolidated_gles(ppcv.normal_balances + ppcv.z_opening_balances, "Balance Sheet")
# build dimension wise dictionary from all GLE's
bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
# save
json_dict = make_dict_json_compliant(bs_dimension_wise_acc_balance)
frappe.db.set_value(
"Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict)
)
return bs_dimension_wise_acc_balance
def get_p_l_closing_entries(pl_gles, pcv):
pl_closing_entries = copy.deepcopy(pl_gles)
for d in pl_gles:
# reverse debit and credit
gle_copy = copy.deepcopy(d)
gle_copy.debit = d.credit
gle_copy.credit = d.debit
gle_copy.debit_in_account_currency = d.credit_in_account_currency
gle_copy.credit_in_account_currency = d.debit_in_account_currency
gle_copy.is_period_closing_voucher_entry = 0
gle_copy.period_closing_voucher = pcv.name
pl_closing_entries.append(gle_copy)
return pl_closing_entries
def get_bs_closing_entries(dimension_wise_balance, pcv):
closing_entries = []
for dimensions, account_balances in dimension_wise_balance.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if acc != "balances" and balance_in_company_currency:
closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions))
return closing_entries
def get_closing_account_closing_entry(closing_account_gle, pcv):
closing_entries_for_closing_account = copy.deepcopy(closing_account_gle)
for d in closing_entries_for_closing_account:
d.period_closing_voucher = pcv.name
return closing_entries_for_closing_account
def summarize_and_post_ledger_entries(docname):
# P&L accounts
pl_accounts_reverse_gle, closing_account_gle = get_gl_entries(docname)
gl_entries = pl_accounts_reverse_gle + closing_account_gle
from erpnext.accounts.general_ledger import make_gl_entries
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
pcv = frappe.get_doc("Period Closing Voucher", pcv_name)
# Balance sheet accounts
bs_dimension_wise_acc_balance = calculate_balance_sheet_balance(docname)
pl_closing_entries = get_p_l_closing_entries(pl_accounts_reverse_gle, pcv)
bs_closing_entries = get_bs_closing_entries(bs_dimension_wise_acc_balance, pcv)
closing_entries_for_closing_account = get_closing_account_closing_entry(closing_account_gle, pcv)
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
def get_closing_entry(pcv, account, balances, dimensions):
closing_entry = frappe._dict(
{
"company": pcv.company,
"closing_date": pcv.period_end_date,
"period_closing_voucher": pcv.name,
"account": account,
"account_currency": balances.account_currency,
"debit_in_account_currency": flt(balances.debit_in_account_currency),
"debit": flt(balances.debit),
"credit_in_account_currency": flt(balances.credit_in_account_currency),
"credit": flt(balances.credit),
"is_period_closing_voucher_entry": 0,
}
)
# update dimensions
update_default_dimensions(get_dimensions(), closing_entry, dimensions)
return closing_entry
def get_dimensions():
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
default_dimensions = ["cost_center", "finance_book", "project"]
dimensions = default_dimensions + get_accounting_dimensions()
return dimensions
def get_dimension_key(res):
return tuple([res.get(dimension) for dimension in get_dimensions()])
def build_dimension_wise_balance_dict(gl_entries):
dimension_balances = frappe._dict()
for x in gl_entries:
dimension_key = get_dimension_key(x)
dimension_balances.setdefault(dimension_key, frappe._dict()).setdefault(
x.account,
frappe._dict(
{
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
"debit": 0,
"credit": 0,
"account_currency": x.account_currency,
}
),
)
dimension_balances[dimension_key][x.account].debit_in_account_currency += flt(
x.debit_in_account_currency
)
dimension_balances[dimension_key][x.account].credit_in_account_currency += flt(
x.credit_in_account_currency
)
dimension_balances[dimension_key][x.account].debit += flt(x.debit)
dimension_balances[dimension_key][x.account].credit += flt(x.credit)
# dimension-wise total balances
dimension_balances[dimension_key].setdefault(
"balances",
frappe._dict(
{
"balance_in_account_currency": 0,
"balance_in_company_currency": 0,
}
),
)
balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency)
balance_in_company_currency = flt(x.debit) - flt(x.credit)
dimension_balances[dimension_key][
"balances"
].balance_in_account_currency += balance_in_account_currency
dimension_balances[dimension_key][
"balances"
].balance_in_company_currency += balance_in_company_currency
return dimension_balances
def process_individual_date(docname: str, date, report_type, parentfield):
current_date_status = frappe.db.get_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
"status",
)
if current_date_status != "Running":
return
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
company = frappe.db.get_value("Period Closing Voucher", pcv_name, "company")
dimensions = get_dimensions()
accounts = frappe.db.get_all(
"Account", filters={"company": company, "report_type": report_type}, pluck="name"
)
# summarize
gle = qb.DocType("GL Entry")
query = qb.from_(gle).select(gle.account)
for dim in dimensions:
query = query.select(gle[dim])
query = query.select(
Sum(gle.debit).as_("debit"),
Sum(gle.credit).as_("credit"),
Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"),
Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"),
gle.account_currency,
).where(
(gle.company.eq(company))
& (gle.is_cancelled.eq(0))
& (gle.posting_date.eq(date))
& (gle.account.isin(accounts))
)
if parentfield == "z_opening_balances":
query = query.where(gle.is_opening.eq("Yes"))
query = query.groupby(gle.account)
for dim in dimensions:
query = query.groupby(gle[dim])
res = query.run(as_dict=True)
# save results
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"closing_balance",
frappe.json.dumps(res),
)
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"status",
"Completed",
)
# chain call
schedule_next_date(docname)

View File

@@ -0,0 +1,4 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-01 15:58:17.544153",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"processing_date",
"report_type",
"status",
"closing_balance"
],
"fields": [
{
"fieldname": "processing_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Processing Date"
},
{
"default": "Queued",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
},
{
"fieldname": "closing_balance",
"fieldtype": "JSON",
"in_list_view": 1,
"label": "Closing Balance"
},
{
"default": "Profit and Loss",
"fieldname": "report_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Report Type",
"options": "Profit and Loss\nBalance Sheet"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-20 12:03:59.106931",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher Detail",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,26 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProcessPeriodClosingVoucherDetail(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
closing_balance: DF.JSON | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
processing_date: DF.Date | None
report_type: DF.Literal["Profit and Loss", "Balance Sheet"]
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
# end: auto-generated types
pass

View File

@@ -2411,6 +2411,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
target.purchase_order = source.purchase_order
target.po_detail = source.purchase_order_item
if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"):
target.use_serial_batch_fields = 1
item_field_map = {
"doctype": target_doctype + " Item",
"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],

View File

@@ -648,6 +648,7 @@ def make_reverse_gl_entries(
adv_adj=False,
update_outstanding="Yes",
partial_cancel=False,
posting_date=None,
):
"""
Get original gl entries of the voucher
@@ -745,6 +746,8 @@ def make_reverse_gl_entries(
if immutable_ledger_enabled:
new_gle["is_cancelled"] = 0
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
elif posting_date:
new_gle["posting_date"] = posting_date
if new_gle["debit"] or new_gle["credit"]:
make_entry(new_gle, adv_adj, "Yes")

View File

@@ -664,7 +664,16 @@ class ReceivablePayableReport:
invoiced = d.base_payment_amount
paid_amount = d.base_paid_amount
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
in_party_currency = self.filters.get("in_party_currency")
# company, billing, and party account currencies are the same
if company_currency == d.currency and company_currency == d.party_account_currency:
in_party_currency = False
# When filtered by party currency and the billing currency not matches the party account currency
if in_party_currency and d.currency != d.party_account_currency:
in_party_currency = False
if in_party_currency:
invoiced = d.payment_amount
paid_amount = d.paid_amount

View File

@@ -199,6 +199,81 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
row = report[1]
self.assertTrue(len(row) == 0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1},
)
def test_allow_multi_currency_invoices_against_single_party_account(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range": "30, 60, 90, 120",
"show_remarks": True,
"in_party_currency": 1,
}
# CASE 1: Company currency and party account currency are the same
si = self.create_sales_invoice(qty=1, no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
filters.update(
{
"party_type": "Customer",
"party": [self.customer],
}
)
report = execute(filters)
row = report[1][0]
expected_data = [8000, 8000, "No Remarks"] # Data in company currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
# CASE 2: Transaction currency and party account currency are the same
self.create_customer(
"USD Customer", currency="USD", default_account=self.debtors_usd, company=self.company
)
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
currency="USD",
conversion_rate=80,
price_list_rate=100,
do_not_save=1,
)
si.save().submit()
filters.update(
{
"party_type": "Customer",
"party": [self.customer],
}
)
report = execute(filters)
row = report[1][0]
expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
# View in Company currency
filters.pop("in_party_currency")
report = execute(filters)
row = report[1][0]
expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
def test_accounts_receivable_with_partial_payment(self):
filters = {
"company": self.company,

View File

@@ -273,6 +273,7 @@ def get_asset_value_adjustment_map_by_category(filters):
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
AND gle.is_opening = 'No'
GROUP BY a.asset_category
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
@@ -543,6 +544,7 @@ def get_asset_value_adjustment_map(filters):
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
AND gle.is_opening = 'No'
GROUP BY a.name
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},

View File

@@ -85,6 +85,12 @@ frappe.query_reports["Gross Profit"] = {
});
},
},
{
fieldname: "include_returned_invoices",
label: __("Include Returned Invoices (Stand-alone)"),
fieldtype: "Check",
default: 1,
},
],
tree: true,
name_field: "parent",

View File

@@ -231,6 +231,15 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
group_columns = group_wise_columns.get(scrub(filters.group_by))
# removing customer_name from group columns
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
if "customer_name" in group_columns and (
supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name"
):
group_columns = [col for col in group_columns if col != "customer_name"]
for src in gross_profit_data.grouped_data:
total_base_amount += src.base_amount or 0.00
total_buying_amount += src.buying_amount or 0.00
@@ -850,7 +859,10 @@ class GrossProfitGenerator:
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
if self.filters.include_returned_invoices:
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
else:
conditions += " and is_return = 0"
if self.filters.item_group:
conditions += f" and {get_item_group_condition(self.filters.item_group)}"

View File

@@ -442,7 +442,11 @@ class TestGrossProfit(FrappeTestCase):
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
company=self.company,
from_date=nowdate(),
to_date=nowdate(),
group_by="Invoice",
include_returned_invoices=1,
)
columns, data = execute(filters=filters)

View File

@@ -5,7 +5,9 @@ from erpnext.stock.doctype.item.test_item import create_item
class AccountsTestMixin:
def create_customer(self, customer_name="_Test Customer", currency=None):
def create_customer(
self, customer_name="_Test Customer", currency=None, default_account=None, company=None
):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
@@ -13,9 +15,28 @@ class AccountsTestMixin:
if currency:
customer.default_currency = currency
if company and default_account:
customer.append(
"accounts",
{
"company": company,
"account": default_account,
},
)
customer.save()
self.customer = customer.name
else:
if company and default_account:
customer = frappe.get_doc("Customer", customer_name)
customer.accounts = []
customer.append(
"accounts",
{
"company": company,
"account": default_account,
},
)
customer.save()
self.customer = customer_name
def create_supplier(self, supplier_name="_Test Supplier", currency=None):

View File

@@ -518,6 +518,7 @@
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:doc.docstatus > 0",
"fieldname": "additional_asset_cost",
"fieldtype": "Currency",
@@ -596,7 +597,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-05-20 00:44:06.229177",
"modified": "2025-10-23 22:43:33.634452",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -641,4 +642,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -69,6 +69,7 @@ class Asset(AccountsController):
default_finance_book: DF.Link | None
department: DF.Link | None
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
depreciation_completed: DF.Check
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"]
disposal_date: DF.Date | None
finance_books: DF.Table[AssetFinanceBook]
@@ -152,7 +153,9 @@ class Asset(AccountsController):
)
self.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount
def before_save(self):
self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
self.status = self.get_status()
def on_submit(self):
@@ -936,7 +939,7 @@ def make_post_gl_entry():
assets = frappe.db.sql_list(
""" select name from `tabAsset`
where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0
and available_for_use_date = %s""",
and available_for_use_date = %s and docstatus = 1""",
(asset_category.name, nowdate()),
)

View File

@@ -644,9 +644,8 @@ class PurchaseOrder(BuyingController):
if not self.is_against_so():
return
for item in removed_items:
prev_ordered_qty = (
prev_ordered_qty = flt(
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
or 0.0
)
frappe.db.set_value(

View File

@@ -2026,7 +2026,7 @@ class AccountsController(TransactionBase):
discount_amount * self.get("conversion_rate"),
item.precision("discount_amount"),
),
dr_or_cr + "_in_account_currency": flt(
dr_or_cr + "_in_transaction_currency": flt(
discount_amount, item.precision("discount_amount")
),
"cost_center": item.cost_center,
@@ -2047,7 +2047,7 @@ class AccountsController(TransactionBase):
discount_amount * self.get("conversion_rate"),
item.precision("discount_amount"),
),
rev_dr_cr + "_in_account_currency": flt(
rev_dr_cr + "_in_transaction_currency": flt(
discount_amount, item.precision("discount_amount")
),
"cost_center": item.cost_center,
@@ -3073,9 +3073,7 @@ def set_balance_in_account_currency(
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
)
gl_dict["account_currency"] = (
company_currency if account_currency == company_currency else account_currency
)
gl_dict["account_currency"] = account_currency
# set debit/credit in account currency if not provided
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
@@ -3663,8 +3661,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def is_allowed_zero_qty():
if parent_doctype == "Sales Order":
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
elif parent_doctype == "Purchase Order":
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
return False
def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")):
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
frappe.throw(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
@@ -3800,6 +3805,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
conv_fac_precision = child_item.precision("conversion_factor") or 2
qty_precision = child_item.precision("qty") or 2
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
rate_unchanged = prev_rate == new_rate
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
# Amount cannot be lesser than billed amount, except for negative amounts
row_rate = flt(d.get("rate"), rate_precision)
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(

View File

@@ -878,6 +878,7 @@ class BuyingController(SubcontractingController):
"asset_category": item_data.get("asset_category"),
"location": row.asset_location,
"company": self.company,
"status": "Draft",
"supplier": self.supplier,
"purchase_date": self.posting_date,
"calculate_depreciation": 0,

View File

@@ -480,6 +480,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.subcontracting_receipt_item = source_doc.name
if return_against_rejected_qty:
target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0))
target_doc.rejected_qty = 0.0
target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty
target_doc.return_qty_from_rejected_warehouse = 1
else:
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_order_item = source_doc.purchase_order_item

View File

@@ -253,6 +253,14 @@ class StockController(AccountsController):
"do_not_submit": True if not via_landed_cost_voucher else False,
}
if self.is_internal_transfer() and row.get("from_warehouse") and not self.is_return:
self.update_bundle_details(bundle_details, table_name, row)
bundle_details["type_of_transaction"] = "Outward"
bundle_details["warehouse"] = row.get("from_warehouse")
bundle_details["qty"] = row.get("stock_qty") or row.get("qty")
self.create_serial_batch_bundle(bundle_details, row)
continue
if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"):
self.update_bundle_details(bundle_details, table_name, row, parent_details=parent_details)
self.create_serial_batch_bundle(bundle_details, row)
@@ -1544,7 +1552,9 @@ def get_stock_ledger_preview(doc, filters):
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
doc.docstatus = 1
doc.make_bundle_using_old_serial_batch_fields()
doc.update_stock_ledger()
columns = get_sl_columns(filters)
sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)

View File

@@ -279,6 +279,7 @@
"oldfieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
@@ -590,6 +591,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_semi_finished_goods === 0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "Finished Goods based Operating Cost"
@@ -638,7 +640,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-06-16 16:13:22.497695",
"modified": "2025-10-29 17:43:12.966753",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -158,6 +158,7 @@ class BOM(WebsiteGenerator):
show_operations: DF.Check
thumbnail: DF.Data | None
total_cost: DF.Currency
track_semi_finished_goods: DF.Check
transfer_material_against: DF.Literal["", "Work Order", "Job Card"]
uom: DF.Link | None
web_long_description: DF.TextEditor | None

View File

@@ -408,7 +408,7 @@ frappe.ui.form.on("Job Card", {
function updateStopwatch(increment) {
var hours = Math.floor(increment / 3600);
var minutes = Math.floor((increment - hours * 3600) / 60);
var seconds = increment - hours * 3600 - minutes * 60;
var seconds = Math.floor(increment - hours * 3600 - minutes * 60);
$(section)
.find(".hours")
@@ -431,7 +431,7 @@ frappe.ui.form.on("Job Card", {
frm.dashboard.refresh();
const timer = `
<div class="stopwatch" style="font-weight:bold;margin:0px 13px 0px 2px;
color:#545454;font-size:18px;display:inline-block;vertical-align:text-bottom;>
color:#545454;font-size:18px;display:inline-block;vertical-align:text-bottom;">
<span class="hours">00</span>
<span class="colon">:</span>
<span class="minutes">00</span>
@@ -441,20 +441,34 @@ frappe.ui.form.on("Job Card", {
var section = frm.toolbar.page.add_inner_message(timer);
let currentIncrement = frm.doc.current_time || 0;
let currentIncrement = frm.events.get_current_time(frm);
if (frm.doc.started_time || frm.doc.current_time) {
if (frm.doc.status == "On Hold") {
updateStopwatch(currentIncrement);
} else {
currentIncrement += moment(frappe.datetime.now_datetime()).diff(
moment(frm.doc.started_time),
"seconds"
);
initialiseTimer();
}
}
},
get_current_time(frm) {
let current_time = 0;
frm.doc.time_logs.forEach((d) => {
if (d.to_time) {
if (d.time_in_mins) {
current_time += flt(d.time_in_mins, 2) * 60;
} else {
current_time += get_seconds_diff(d.to_time, d.from_time);
}
} else {
current_time += get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
}
});
return current_time;
},
hide_timer: function (frm) {
frm.toolbar.page.inner_toolbar.find(".stopwatch").remove();
},
@@ -519,3 +533,7 @@ frappe.ui.form.on("Job Card Time Log", {
frm.set_value("started_time", "");
},
});
function get_seconds_diff(d1, d2) {
return moment(d1).diff(d2, "seconds");
}

View File

@@ -724,6 +724,9 @@ class ProductionPlan(Document):
if not wo_list:
frappe.msgprint(_("No Work Orders were created"))
if not po_list:
frappe.msgprint(_("No Purchase Orders were created"))
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
items_data = self.get_production_items()

View File

@@ -26,7 +26,6 @@ def get_exploded_items(bom, data, indent=0, qty=1):
)
for item in exploded_items:
print(item.bom_no, indent)
item["indent"] = indent
data.append(
{

View File

@@ -422,3 +422,5 @@ execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_fo
erpnext.patches.v15_0.add_company_payment_gateway_account
erpnext.patches.v15_0.update_uae_zero_rated_fetch
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing

View File

@@ -0,0 +1,13 @@
import frappe
from frappe.query_builder import DocType
def execute():
Asset = DocType("Asset")
query = (
frappe.qb.update(Asset)
.set(Asset.status, "Draft")
.where((Asset.docstatus == 0) & ((Asset.status.isnull()) | (Asset.status == "")))
)
query.run()

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
"""
Description:
Enable Legacy controller for Period Closing Voucher
"""
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)

View File

@@ -245,6 +245,7 @@
"fieldname": "act_start_date",
"fieldtype": "Date",
"label": "Actual Start Date (via Timesheet)",
"no_copy": 1,
"oldfieldname": "act_start_date",
"oldfieldtype": "Date",
"read_only": 1
@@ -253,6 +254,7 @@
"fieldname": "actual_time",
"fieldtype": "Float",
"label": "Actual Time in Hours (via Timesheet)",
"no_copy": 1,
"read_only": 1
},
{
@@ -263,6 +265,7 @@
"fieldname": "act_end_date",
"fieldtype": "Date",
"label": "Actual End Date (via Timesheet)",
"no_copy": 1,
"oldfieldname": "act_end_date",
"oldfieldtype": "Date",
"read_only": 1
@@ -277,6 +280,7 @@
"fieldname": "total_costing_amount",
"fieldtype": "Currency",
"label": "Total Costing Amount (via Timesheet)",
"no_copy": 1,
"oldfieldname": "actual_budget",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -290,6 +294,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billable Amount (via Timesheet)",
"no_copy": 1,
"read_only": 1
},
{
@@ -399,7 +404,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2024-01-08 16:00:41.296203",
"modified": "2025-10-16 08:39:12.214577",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
@@ -429,4 +434,4 @@
"timeline_field": "project",
"title_field": "subject",
"track_seen": 1
}
}

View File

@@ -3,6 +3,12 @@
frappe.query_reports["Delayed Tasks Summary"] = {
filters: [
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project",
},
{
fieldname: "from_date",
label: __("From Date"),

View File

@@ -23,6 +23,7 @@ def get_data(filters):
fields=[
"name",
"subject",
"project",
"exp_start_date",
"exp_end_date",
"status",
@@ -56,7 +57,7 @@ def get_data(filters):
def get_conditions(filters):
conditions = frappe._dict()
keys = ["priority", "status"]
keys = ["priority", "status", "project"]
for key in keys:
if filters.get(key):
conditions[key] = filters.get(key)
@@ -89,6 +90,13 @@ def get_columns():
columns = [
{"fieldname": "name", "fieldtype": "Link", "label": _("Task"), "options": "Task", "width": 150},
{"fieldname": "subject", "fieldtype": "Data", "label": _("Subject"), "width": 200},
{
"fieldname": "project",
"fieldtype": "Link",
"label": _("Project"),
"options": "Project",
"width": 150,
},
{"fieldname": "status", "fieldtype": "Data", "label": _("Status"), "width": 100},
{"fieldname": "priority", "fieldtype": "Data", "label": _("Priority"), "width": 80},
{"fieldname": "progress", "fieldtype": "Data", "label": _("Progress (%)"), "width": 120},

View File

@@ -237,10 +237,10 @@ erpnext.accounts.pos = {
frappe.ui.form.on(doctype, {
mode_of_payment: function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
get_payment_mode_account(frm, d.mode_of_payment, function(account){
frappe.model.set_value(cdt, cdn, 'account', account)
})
}
erpnext.accounts.pos.get_payment_mode_account(frm, d.mode_of_payment, function (account) {
frappe.model.set_value(cdt, cdn, "account", account);
});
},
});
},

View File

@@ -394,6 +394,10 @@ erpnext.PointOfSale.Payment = class {
const payments = doc.payments;
const currency = doc.currency;
if (!this.$payment_modes.is(":visible")) {
return;
}
this.$payment_modes.html(
`${payments
.map((p, i) => {
@@ -612,6 +616,10 @@ erpnext.PointOfSale.Payment = class {
const currency = doc.currency;
const label = __("Change Amount");
if (!this.$totals.is(":visible")) {
return;
}
this.$totals.html(
`<div class="col">
<div class="total-label">${__("Grand Total")}</div>

View File

@@ -181,11 +181,35 @@ class Company(NestedSet):
["Stock Received But Not Billed Account", "stock_received_but_not_billed"],
["Stock Adjustment Account", "stock_adjustment_account"],
["Expense Included In Valuation Account", "expenses_included_in_valuation"],
["Write Off Account", "write_off_account"],
["Default Payment Discount Account", "default_discount_account"],
["Unrealized Profit / Loss Account", "unrealized_profit_loss_account"],
["Exchange Gain / Loss Account", "exchange_gain_loss_account"],
["Unrealized Exchange Gain / Loss Account", "unrealized_exchange_gain_loss_account"],
["Round Off Account", "round_off_account"],
["Default Deferred Revenue Account", "default_deferred_revenue_account"],
["Default Deferred Expense Account", "default_deferred_expense_account"],
["Accumulated Depreciation Account", "accumulated_depreciation_account"],
["Depreciation Expense Account", "depreciation_expense_account"],
["Gain/Loss Account on Asset Disposal", "disposal_account"],
]
for account in accounts:
if self.get(account[1]):
for_company = frappe.db.get_value("Account", self.get(account[1]), "company")
for_company, is_group, disabled = frappe.db.get_value(
"Account", self.get(account[1]), ["company", "is_group", "disabled"]
)
if disabled:
frappe.throw(_("Account {0} is disabled.").format(frappe.bold(self.get(account[1]))))
if is_group:
frappe.throw(
_("{0}: {1} is a group account.").format(
frappe.bold(account[0]), frappe.bold(self.get(account[1]))
)
)
if for_company != self.name:
frappe.throw(
_("Account {0} does not belong to company: {1}").format(
@@ -763,13 +787,11 @@ def update_company_current_month_sales(company):
def update_company_monthly_sales(company):
"""Cache past year monthly sales of every company based on sales invoices"""
import json
from frappe.utils.goal import get_monthly_results
filter_str = f"company = {frappe.db.escape(company)} and status != 'Draft' and docstatus=1"
filter_dict = {"company": company, "status": ["!=", "Draft"], "docstatus": 1}
month_to_value_dict = get_monthly_results(
"Sales Invoice", "base_grand_total", "posting_date", filter_str, "sum"
"Sales Invoice", "base_grand_total", "posting_date", filter_dict, "sum"
)
frappe.db.set_value("Company", company, "sales_monthly_history", json.dumps(month_to_value_dict))

View File

@@ -24,6 +24,7 @@ frappe.ui.form.on("Driver", {
transporter: function (frm, cdt, cdn) {
// this assumes that supplier's address has same title as supplier's name
if (!frm.doc.transporter) return;
frappe.db
.get_doc("Address", null, { address_title: frm.doc.transporter })
.then((r) => {

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": "modified",
@@ -871,4 +873,4 @@
"states": [],
"title_field": "employee_name",
"track_changes": 1
}
}

View File

@@ -17,6 +17,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.party import get_due_date
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.stock_ledger import validate_reserved_stock
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -469,6 +470,10 @@ class DeliveryNote(SellingController):
self.make_bundle_using_old_serial_batch_fields(table_name)
self.validate_standalone_serial_nos_customer()
if not self.is_return:
self.validate_reserved_stock()
self.update_stock_reservation_entries()
# Updating stock ledger should always be called after updating prevdoc status,
@@ -506,6 +511,66 @@ class DeliveryNote(SellingController):
self.delete_auto_created_batches()
def validate_reserved_stock(self):
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_against_so_for_dn,
)
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
# fetch reserved stock data from bin
reserved_stocks = self.get_reserved_stock_details()
for row in self.items:
if reserved_stocks.get((row.item_code, row.warehouse)) > 0:
args = frappe._dict(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"batch_nos": [row.batch_no] if row.batch_no else [],
"serial_nos": row.serial_no.split("\n") if row.serial_no else [],
"serial_and_batch_bundle": row.serial_and_batch_bundle,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"actual_qty": row.qty * -1,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
if row.against_sales_order and row.so_detail:
args.ignore_voucher_nos = get_sre_against_so_for_dn(
row.against_sales_order, row.so_detail
)
validate_reserved_stock(args)
def get_reserved_stock_details(self):
"""
Create dict from bin based on item and warehouse:
{(item_code, warehouse): reserved_stock}
Use: to quickly retrieve/check reserved stock value instead of looping n times
"""
item_codes = set()
warehouses = set()
for row in self.items:
item_codes.add(row.item_code)
warehouses.add(row.warehouse)
bins = frappe.db.get_all(
"Bin",
{"item_code": ["in", item_codes], "warehouse": ["in", warehouses]},
["item_code", "warehouse", "reserved_stock"],
)
reserved_stock_lookup = {(b.item_code, b.warehouse): flt(b.reserved_stock) for b in bins}
return reserved_stock_lookup
def validate_against_stock_reservation_entries(self):
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
@@ -1310,6 +1375,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if source.get("use_serial_batch_fields"):
target.set("use_serial_batch_fields", 1)
if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"):
target.set("use_serial_batch_fields", 1)
doclist = get_mapped_doc(
doctype,
source_name,

View File

@@ -39,7 +39,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
set_valuation_method,
)
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse, get_warehouse
from erpnext.stock.stock_ledger import get_previous_sle
@@ -2719,6 +2719,75 @@ class TestDeliveryNote(FrappeTestCase):
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
)
@change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1})
def test_partial_delivery_note_against_reserved_stock(self):
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_stock_reservation_entries_for_voucher,
)
# create batch item
batch_item = make_item(
"_Test Batch Item For DN Reserve Check",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBDNR.#####",
},
)
serial_item = make_item(
"_Test Serial Item For DN Reserve Check",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TSNDNR.#####",
},
)
company = "_Test Company"
warehouse = create_warehouse("Test Partial DN Reserved Stock", company=company)
customer = "_Test Customer"
items = [batch_item.name, serial_item.name]
for idx, item in enumerate(items):
# make inward entry for batch item
se = make_stock_entry(item_code=item, purpose="Material Receipt", qty=10, to_warehouse=warehouse)
sabb = se.items[0].serial_and_batch_bundle
batch_no = get_batch_from_bundle(sabb) if not idx else None
serial_nos = get_serial_nos_from_bundle(sabb) if idx else None
# make sales order and reserve the quantites against the so
so = make_sales_order(item_code=item, qty=10, rate=100, customer=customer, warehouse=warehouse)
so.submit()
so.create_stock_reservation_entries()
so.reload()
# create a delivery note with partial quantity from resreved quantity
dn = create_dn_against_so(so=so.name, delivered_qty=5, do_not_submit=True)
dn.items[0].use_serial_batch_fields = 1
if batch_no:
dn.items[0].batch_no = batch_no
else:
dn.items[0].serial_no = "\n".join(serial_nos[:5])
dn.save()
dn.submit()
against_sales_order = dn.items[0].against_sales_order
so_detail = dn.items[0].so_detail
sre_details = get_stock_reservation_entries_for_voucher(
so.doctype, against_sales_order, so_detail, ["reserved_qty", "delivered_qty", "status"]
)
# check partially delivered reserved stock
self.assertEqual(sre_details[0].status, "Partially Delivered")
self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty)
self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -4287,6 +4287,67 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
@change_settings(
"Buying Settings",
{"bill_for_rejected_quantity_in_purchase_invoice": 1, "set_valuation_rate_for_rejected_materials": 1},
)
def test_valuation_rate_for_rejected_materials_with_serial_no(self):
item = make_item(
"Test Serial Item with Rej Material Valuation",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SNU-TSIRMV-.#####"},
)
company = "_Test Company with perpetual inventory"
warehouse = create_warehouse(
"_Test In-ward Warehouse",
company="_Test Company with perpetual inventory",
)
rej_warehouse = create_warehouse(
"_Test Warehouse - Rejected Material",
company="_Test Company with perpetual inventory",
)
pr = make_purchase_receipt(
item_code=item.name,
qty=10,
rate=100,
company=company,
warehouse=warehouse,
rejected_qty=5,
rejected_warehouse=rej_warehouse,
)
stock_received_but_not_billed_account = frappe.get_value(
"Company",
company,
"stock_received_but_not_billed",
)
rejected_item_cost = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"warehouse": rej_warehouse,
},
"stock_value_difference",
)
self.assertEqual(rejected_item_cost, 500)
srbnb_cost = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"account": stock_received_but_not_billed_account,
},
"credit",
)
self.assertEqual(srbnb_cost, 1500)
def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self):
item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"

View File

@@ -253,6 +253,9 @@ class SerialandBatchBundle(Document):
}
)
if self.voucher_type == "Delivery Note":
kwargs["ignore_voucher_nos"] = self.get_sre_against_dn()
available_serial_nos = get_available_serial_nos(frappe._dict(kwargs))
serial_no_warehouse = {}
@@ -667,8 +670,12 @@ class SerialandBatchBundle(Document):
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
set_valuation_rate_for_rejected_materials = frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
)
for d in self.entries:
if self.is_rejected:
if self.is_rejected and not set_valuation_rate_for_rejected_materials:
rate = 0.0
elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference:
continue
@@ -1376,6 +1383,20 @@ class SerialandBatchBundle(Document):
self.set("entries", [])
def get_sre_against_dn(self):
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_against_so_for_dn,
)
so_name, so_detail_no = frappe.db.get_value(
"Delivery Note Item", self.voucher_detail_no, ["against_sales_order", "so_detail"]
)
if so_name and so_detail_no:
sre_names = get_sre_against_so_for_dn(so_name, so_detail_no)
return sre_names
@frappe.whitelist()
def download_blank_csv_template(content):

View File

@@ -539,7 +539,7 @@ frappe.ui.form.on("Stock Entry", {
const item = locals[cdt][cdn];
item.transfer_qty = flt(item.qty) * flt(item.conversion_factor);
const args = {
let args = {
item_code: item.item_code,
posting_date: frm.doc.posting_date,
posting_time: frm.doc.posting_time,
@@ -553,6 +553,10 @@ frappe.ui.form.on("Stock Entry", {
allow_zero_valuation: 1,
};
if (item.batch_no && frm.doc.purpose == "Material Receipt") {
args.qty = Math.abs(args.qty) * -1;
}
if (item.item_code || item.serial_no) {
frappe.call({
method: "erpnext.stock.utils.get_incoming_rate",

View File

@@ -241,6 +241,8 @@ class StockEntry(StockController):
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.validate_same_source_target_warehouse_during_material_transfer()
def on_submit(self):
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
@@ -796,6 +798,53 @@ class StockEntry(StockController):
title=_("Missing Item"),
)
def validate_same_source_target_warehouse_during_material_transfer(self):
"""
Validate Material Transfer entries where source and target warehouses are identical.
For Material Transfer purpose, if an item has the same source and target warehouse,
require that at least one inventory dimension (if configured) differs between source
and target to ensure a meaningful transfer is occurring.
Raises:
frappe.ValidationError: If warehouses are same and no inventory dimensions differ
"""
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
inventory_dimensions = get_inventory_dimensions()
if self.purpose == "Material Transfer":
for item in self.items:
if cstr(item.s_warehouse) == cstr(item.t_warehouse):
if not inventory_dimensions:
frappe.throw(
_(
"Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer"
).format(item.idx),
title=_("Invalid Source and Target Warehouse"),
)
else:
difference_found = False
for dimension in inventory_dimensions:
fieldname = (
dimension.source_fieldname
if dimension.source_fieldname.startswith("to_")
else f"to_{dimension.source_fieldname}"
)
if (
item.get(dimension.source_fieldname)
and item.get(fieldname)
and item.get(dimension.source_fieldname) != item.get(fieldname)
):
difference_found = True
break
if not difference_found:
frappe.throw(
_(
"Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer"
).format(item.idx),
title=_("Invalid Source and Target Warehouse"),
)
def get_matched_items(self, item_code):
for row in self.items:
if row.item_code == item_code or row.original_item == item_code:

View File

@@ -7,7 +7,10 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Stock Reconciliation", {
setup(frm) {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm });
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: frm,
uom_field: "stock_uom",
});
},
onload: function (frm) {

View File

@@ -738,7 +738,7 @@ def get_sre_reserved_qty_for_voucher_detail_no(
def get_sre_reserved_serial_nos_details(
item_code: str, warehouse: str, serial_nos: list | None = None
item_code: str, warehouse: str, serial_nos: list | None = None, ignore_voucher_nos: list | None = None
) -> dict:
"""Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}"""
@@ -753,8 +753,7 @@ def get_sre_reserved_serial_nos_details(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& (sre.reserved_qty > sre.delivered_qty)
& (sre.status.notin(["Delivered", "Cancelled"]))
& (sre.delivered_qty < sre.reserved_qty)
& (sre.reservation_based_on == "Serial and Batch")
)
.orderby(sb_entry.creation)
@@ -763,10 +762,15 @@ def get_sre_reserved_serial_nos_details(
if serial_nos:
query = query.where(sb_entry.serial_no.isin(serial_nos))
if ignore_voucher_nos:
query = query.where(sre.name.notin(ignore_voucher_nos))
return frappe._dict(query.run())
def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos: list | None = None) -> dict:
def get_sre_reserved_batch_nos_details(
item_code: str, warehouse: str, batch_nos: list | None = None, ignore_voucher_nos: list | None = None
) -> dict:
"""Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}"""
sre = frappe.qb.DocType("Stock Reservation Entry")
@@ -784,7 +788,7 @@ def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& ((sre.reserved_qty - sre.delivered_qty) > 0)
& (sre.status.notin(["Delivered", "Cancelled"]))
& (sre.delivered_qty < sre.reserved_qty)
& (sre.reservation_based_on == "Serial and Batch")
)
.groupby(sb_entry.batch_no)
@@ -794,6 +798,9 @@ def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos
if batch_nos:
query = query.where(sb_entry.batch_no.isin(batch_nos))
if ignore_voucher_nos:
query = query.where(sre.name.notin(ignore_voucher_nos))
return frappe._dict(query.run())
@@ -1175,3 +1182,24 @@ def get_stock_reservation_entries_for_voucher(
query = query.where(sre.status.notin(["Delivered", "Cancelled"]))
return query.run(as_dict=True)
@frappe.request_cache
def get_sre_against_so_for_dn(so_name: str, so_detail_no: str) -> list[str]:
"""Returns list of Stock Reservation Entries against Delivery Note with Sales Order Reference."""
sre = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.from_(sre)
.select(sre.name)
.where(
(sre.docstatus == 1)
& (sre.voucher_type == "Sales Order")
& (sre.voucher_no == so_name)
& (sre.voucher_detail_no == so_detail_no)
)
)
result = query.run(as_list=True)
return result[0] if result else []

View File

@@ -36,17 +36,6 @@
"show_barcode_field",
"clean_description_html",
"allow_internal_transfer_at_arms_length_price",
"quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
"column_break_23",
"action_if_quality_inspection_is_rejected",
"stock_reservation_tab",
"enable_stock_reservation",
"column_break_rx3e",
"allow_partial_reservation",
"auto_reserve_stock_for_sales_order_on_purchase",
"serial_and_batch_reservation_section",
"auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab",
"section_break_7",
"allow_existing_serial_no",
@@ -548,8 +537,8 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-06 02:39:24.284587",
"modified_by": "Administrator",
"modified": "2025-10-17 18:32:35.829395",
"modified_by": "hello@aerele.in",
"module": "Stock",
"name": "Stock Settings",
"owner": "Administrator",
@@ -569,8 +558,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -24,24 +24,26 @@ frappe.query_reports["Incorrect Serial and Batch Bundle"] = {
},
onload(report) {
report.page.add_inner_button(__("Remove SABB Entry"), () => {
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
report.page
.add_inner_button(__("Fix SABB Entry"), () => {
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select a row to create a Reposting Entry"));
} else {
frappe.call({
method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.remove_sabb_entry",
freeze: true,
args: {
selected_rows: selected_rows,
},
callback: function (r) {
frappe.query_report.refresh();
},
});
}
});
if (!selected_rows.length) {
frappe.throw(__("Please select at least one row to fix"));
} else {
frappe.call({
method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.fix_sabb_entries",
freeze: true,
args: {
selected_rows: selected_rows,
},
callback: function (r) {
frappe.query_report.refresh();
},
});
}
})
.addClass("btn-primary");
},
};

View File

@@ -13,7 +13,10 @@ def execute(filters: dict | None = None):
every time the report is refreshed or a filter is updated.
"""
columns = get_columns()
data = get_data(filters)
unlinked_bundles = get_unlinked_serial_batch_bundles(filters) or []
linked_cancelled_bundles = get_linked_cancelled_sabb(filters) or []
data = unlinked_bundles + linked_cancelled_bundles
return columns, data
@@ -50,14 +53,17 @@ def get_columns() -> list[dict]:
"fieldtype": "Data",
"width": 200,
},
{
"label": _("Is Cancelled"),
"fieldname": "is_cancelled",
"fieldtype": "Check",
"width": 200,
},
]
def get_data(filters) -> list[list]:
"""Return data for the report.
The report data is a list of rows, with each row being a list of cell values.
"""
def get_unlinked_serial_batch_bundles(filters) -> list[list]:
# SABB has not been linked to any SLE
SABB = frappe.qb.DocType("Serial and Batch Bundle")
SLE = frappe.qb.DocType("Stock Ledger Entry")
@@ -77,6 +83,7 @@ def get_data(filters) -> list[list]:
SABB.voucher_type,
SABB.voucher_no,
SABB.voucher_detail_no,
SABB.is_cancelled,
)
.where(
(SLE.serial_and_batch_bundle.isnull())
@@ -94,14 +101,63 @@ def get_data(filters) -> list[list]:
return data
def get_linked_cancelled_sabb(filters):
# SABB has cancelled but voucher is not cancelled
SABB = frappe.qb.DocType("Serial and Batch Bundle")
SLE = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(SABB)
.inner_join(SLE)
.on(SABB.name == SLE.serial_and_batch_bundle)
.select(
SABB.name,
SABB.voucher_type,
SABB.voucher_no,
SABB.voucher_detail_no,
SABB.is_cancelled,
)
.where(
(SLE.serial_and_batch_bundle.isnotnull())
& (SABB.docstatus == 2)
& (SABB.is_cancelled == 1)
& (SLE.is_cancelled == 0)
)
)
for field in filters:
query = query.where(SABB[field] == filters[field])
data = query.run(as_dict=1)
return data
@frappe.whitelist()
def remove_sabb_entry(selected_rows):
def fix_sabb_entries(selected_rows):
if isinstance(selected_rows, str):
selected_rows = frappe.parse_json(selected_rows)
for row in selected_rows:
doc = frappe.get_doc("Serial and Batch Bundle", row.get("name"))
doc.cancel()
doc.delete()
if doc.is_cancelled == 0 and not frappe.db.get_value(
"Stock Ledger Entry",
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
"name",
):
doc.db_set({"is_cancelled": 1, "docstatus": 2})
frappe.msgprint(_("Selected Serial and Batch Bundle entries have been removed."))
for row in doc.entries:
row.db_set("docstatus", 2)
elif doc.is_cancelled == 1 and frappe.db.get_value(
"Stock Ledger Entry",
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
"name",
):
doc.db_set({"is_cancelled": 0, "docstatus": 1})
for row in doc.entries:
row.db_set("docstatus", 1)
frappe.msgprint(_("Selected Serial and Batch Bundle entries have been fixed."))

View File

@@ -18,16 +18,41 @@ def execute(filters=None):
def get_data(filters):
data = get_stock_ledger_entries(filters)
serial_nos_data = prepare_serial_nos(data)
bundles = get_bundles(data)
serial_nos_data = prepare_serial_nos(data, bundles)
data = get_incorrect_serial_nos(serial_nos_data)
return data
def prepare_serial_nos(data):
def get_bundles(data):
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
bundle_dict = frappe._dict()
serial_nos_data = frappe.get_all(
"Serial and Batch Entry",
fields=["parent", "serial_no", "incoming_rate", "qty"],
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
)
for entry in serial_nos_data:
bundle_dict.setdefault(entry.parent, []).append(entry)
return bundle_dict
def prepare_serial_nos(data, bundles):
serial_no_wise_data = {}
for row in data:
if not row.serial_nos:
if not row.serial_nos and not row.serial_and_batch_bundle:
continue
if row.serial_and_batch_bundle:
for bundle in bundles.get(row.serial_and_batch_bundle, []):
sle = copy.deepcopy(row)
sle.serial_no = bundle.serial_no
sle.qty = bundle.qty
sle.valuation_rate = bundle.incoming_rate * (1 if sle.qty > 0 else -1)
serial_no_wise_data.setdefault(bundle.serial_no, []).append(sle)
continue
for serial_no in get_serial_nos(row.serial_nos):
@@ -54,6 +79,9 @@ def get_incorrect_serial_nos(serial_nos_data):
total_value.qty += total_dict.qty
total_value.valuation_rate += total_dict.valuation_rate
if total_dict.qty == 0 and abs(total_dict.valuation_rate) == 0:
continue
result.append(total_dict)
result.append({})
@@ -81,6 +109,7 @@ def get_stock_ledger_entries(report_filters):
"voucher_no",
"item_code",
"serial_no as serial_nos",
"serial_and_batch_bundle",
"actual_qty",
"posting_date",
"posting_time",
@@ -89,7 +118,8 @@ def get_stock_ledger_entries(report_filters):
"(stock_value_difference / actual_qty) as valuation_rate",
]
filters = {"serial_no": ("is", "set"), "is_cancelled": 0}
filters = {"is_cancelled": 0}
or_filters = {"serial_no": ("is", "set"), "serial_and_batch_bundle": ("is", "set")}
if report_filters.get("item_code"):
filters["item_code"] = report_filters.get("item_code")
@@ -104,6 +134,7 @@ def get_stock_ledger_entries(report_filters):
"Stock Ledger Entry",
fields=fields,
filters=filters,
or_filters=or_filters,
order_by="posting_date asc, posting_time asc, creation asc",
)

View File

@@ -731,13 +731,55 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
self.available_qty[ledger.batch_no] += flt(ledger.qty)
self.total_qty[ledger.batch_no] += flt(ledger.total_qty)
entries = self.get_batch_wise_total_available_qty()
for row in entries:
self.total_qty[row.batch_no] += flt(row.total_qty)
self.calculate_avg_rate_from_deprecarated_ledgers()
self.calculate_avg_rate_for_non_batchwise_valuation()
self.set_stock_value_difference()
def get_batch_wise_total_available_qty(self) -> list[dict]:
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
if not self.batchwise_valuation_batches:
return []
parent = frappe.qb.DocType("Serial and Batch Bundle")
child = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(parent)
.inner_join(child)
.on(parent.name == child.parent)
.select(
child.batch_no,
Sum(child.qty).as_("total_qty"),
)
.where(
(parent.warehouse == self.sle.warehouse)
& (parent.item_code == self.sle.item_code)
& (child.batch_no.isin(self.batchwise_valuation_batches))
& (parent.docstatus == 1)
& (parent.is_cancelled == 0)
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
.groupby(child.batch_no)
)
# Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
if self.sle.voucher_detail_no:
query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
elif self.sle.voucher_no:
query = query.where(parent.voucher_no != self.sle.voucher_no)
query = query.where(parent.voucher_type != "Pick List")
return query.run(as_dict=True)
def get_batch_no_ledgers(self) -> list[dict]:
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
if not self.batchwise_valuation_batches:
return []
@@ -765,11 +807,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
.on(parent.name == child.parent)
.select(
child.batch_no,
Sum(Case().when(timestamp_condition, child.stock_value_difference).else_(0)).as_(
"incoming_rate"
),
Sum(Case().when(timestamp_condition, child.qty).else_(0)).as_("qty"),
Sum(child.qty).as_("total_qty"),
Sum(child.stock_value_difference).as_("incoming_rate"),
Sum(child.qty).as_("qty"),
)
.where(
(parent.warehouse == self.sle.warehouse)
@@ -790,6 +829,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
query = query.where(parent.voucher_no != self.sle.voucher_no)
query = query.where(parent.voucher_type != "Pick List")
if timestamp_condition:
query = query.where(timestamp_condition)
return query.run(as_dict=True)

View File

@@ -893,10 +893,19 @@ class update_entries_after:
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
if (
sle.is_adjustment_entry
and flt(sle.qty_after_transaction, self.flt_precision) == 0
and flt(sle.stock_value, self.currency_precision) != 0
):
sle.stock_value_difference = (
get_stock_value_difference(
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
sle.item_code,
sle.warehouse,
sle.posting_date,
sle.posting_time,
voucher_detail_no=sle.voucher_detail_no,
creation=sle.creation,
)
* -1
)
@@ -1905,7 +1914,7 @@ def get_valuation_rate(
)
last_valuation_rate = query.run()
if last_valuation_rate:
if last_valuation_rate and last_valuation_rate[0][0] is not None:
return flt(last_valuation_rate[0][0])
# Get moving average rate of a specific batch number
@@ -2157,7 +2166,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
)
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
if args.reserved_stock:
if args.reserved_stock and args.voucher_type != "Delivery Note":
validate_reserved_stock(args)
@@ -2227,11 +2236,10 @@ def get_future_sle_with_negative_batch_qty(sle_args):
def validate_reserved_stock(kwargs):
if kwargs.serial_no:
serial_nos = kwargs.serial_no.split("\n")
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
validate_reserved_serial_nos(kwargs)
elif kwargs.batch_no:
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
validate_reserved_batch_nos(kwargs)
elif kwargs.serial_and_batch_bundle:
sbb_entries = frappe.db.get_all(
@@ -2245,9 +2253,11 @@ def validate_reserved_stock(kwargs):
)
if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
kwargs.serial_nos = serial_nos
validate_reserved_serial_nos(kwargs)
elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
kwargs.batch_nos = batch_nos
validate_reserved_batch_nos(kwargs)
# Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
precision = cint(frappe.db.get_default("float_precision")) or 2
@@ -2265,9 +2275,13 @@ def validate_reserved_stock(kwargs):
frappe.throw(msg, title=_("Reserved Stock"))
def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(item_code, warehouse, serial_nos):
if common_serial_nos := list(set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))):
def validate_reserved_serial_nos(kwargs):
if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
kwargs.item_code, kwargs.warehouse, kwargs.serial_nos, kwargs.ignore_voucher_nos
):
if common_serial_nos := list(
set(kwargs.serial_nos).intersection(set(reserved_serial_nos_details.keys()))
):
msg = _(
"Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
)
@@ -2281,22 +2295,25 @@ def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
frappe.throw(msg, title=_("Reserved Serial No."))
def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
def validate_reserved_batch_nos(kwargs):
if reserved_batches_map := get_sre_reserved_batch_nos_details(
kwargs.item_code, kwargs.warehouse, kwargs.batch_nos, kwargs.ignore_voucher_nos
):
available_batches = get_auto_batch_nos(
frappe._dict(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
"item_code": kwargs.item_code,
"warehouse": kwargs.warehouse,
"posting_date": kwargs.posting_date,
"posting_time": kwargs.posting_time,
"ignore_voucher_nos": kwargs.ignore_voucher_nos,
}
)
)
available_batches_map = {row.batch_no: row.qty for row in available_batches}
precision = cint(frappe.db.get_default("float_precision")) or 2
for batch_no in batch_nos:
for batch_no in kwargs.batch_nos:
diff = flt(
available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
)
@@ -2304,7 +2321,7 @@ def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
abs(diff),
frappe.get_desk_link("Batch", batch_no),
frappe.get_desk_link("Warehouse", warehouse),
frappe.get_desk_link("Warehouse", kwargs.warehouse),
nowdate(),
nowtime(),
)
@@ -2353,24 +2370,32 @@ def is_internal_transfer(sle):
return True
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
def get_stock_value_difference(
item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None, creation=None
):
table = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(posting_date, posting_time)
query = (
frappe.qb.from_(table)
.select(Sum(table.stock_value_difference).as_("value"))
.where(
(table.is_cancelled == 0)
& (table.item_code == item_code)
& (table.warehouse == warehouse)
& (table.posting_datetime <= posting_datetime)
)
.where((table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse))
)
if voucher_no:
if voucher_detail_no:
query = query.where(table.voucher_detail_no != voucher_detail_no)
elif voucher_no:
query = query.where(table.voucher_no != voucher_no)
if creation:
query = query.where(
(table.posting_datetime < posting_datetime)
| ((table.posting_datetime == posting_datetime) & (table.creation < creation))
)
else:
query = query.where(table.posting_datetime <= posting_datetime)
difference_amount = query.run()
return flt(difference_amount[0][0]) if difference_amount else 0

View File

@@ -82,10 +82,53 @@ frappe.ui.form.on("Subcontracting Receipt", {
frm.add_custom_button(
__("Subcontract Return"),
() => {
frappe.model.open_mapped_doc({
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return",
frm: frm,
const make_standard_return = () => {
frappe.model.open_mapped_doc({
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return",
frm: frm,
});
};
let has_rejected_items = frm.doc.items.filter((item) => {
if (item.rejected_qty > 0) {
return true;
}
});
if (has_rejected_items && has_rejected_items.length > 0) {
frappe.prompt(
[
{
label: __("Return Qty from Rejected Warehouse"),
fieldtype: "Check",
fieldname: "return_for_rejected_warehouse",
default: 1,
},
],
function (values) {
if (values.return_for_rejected_warehouse) {
frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return_against_rejected_warehouse",
args: {
source_name: frm.doc.name,
},
callback: function (r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
},
});
} else {
make_standard_return();
}
},
__("Return Qty"),
__("Make Return Entry")
);
} else {
make_standard_return();
}
},
__("Create")
);

View File

@@ -774,6 +774,13 @@ class SubcontractingReceipt(SubcontractingController):
make_purchase_receipt(self, save=True, notify=True)
@frappe.whitelist()
def make_subcontract_return_against_rejected_warehouse(source_name):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True)
@frappe.whitelist()
def make_subcontract_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc

View File

@@ -1196,6 +1196,136 @@ class TestSubcontractingReceipt(FrappeTestCase):
scr.cancel()
self.assertTrue(scr.docstatus == 2)
def test_subcontract_return_from_rejected_warehouse(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
make_subcontract_return_against_rejected_warehouse,
)
# Create subcontracted item
fg_item = make_item(
"_Test Subcontract Item Return from Rejected Warehouse",
properties={
"is_stock_item": 1,
"is_sub_contracted_item": 1,
},
).name
# Create service item
service_item = make_item(
"_Test Service Item Return from Rejected Warehouse", properties={"is_stock_item": 0}
).name
# Create BOM for the subcontracted item with required raw materials
rm_item1 = make_item(
"_Test RM Item 1 Return from Rejected Warehouse", properties={"is_stock_item": 1}
).name
rm_item2 = make_item(
"_Test RM Item 2 Return from Rejected Warehouse", properties={"is_stock_item": 1}
).name
make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
# Create warehouses
rejected_warehouse = create_warehouse("_Test Subcontract Rejected Warehouse Return Qty Warehouse")
# Create service items for subcontracting order
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": service_item,
"qty": 10,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 10,
},
]
# Create Subcontracting Order
sco = get_subcontracting_order(service_items=service_items)
# Stock raw materials
make_stock_entry(item_code=rm_item1, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
make_stock_entry(item_code=rm_item2, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
# Transfer raw materials
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
# Step 1: Create Subcontracting Receipt with rejected quantity
sr = make_subcontracting_receipt(sco.name)
sr.items[0].qty = 8 # Accepted quantity
sr.items[0].rejected_qty = 2
sr.items[0].rejected_warehouse = rejected_warehouse
sr.save()
sr.submit()
# Verify initial state
sr.reload()
self.assertEqual(sr.items[0].qty, 8)
self.assertEqual(sr.items[0].rejected_qty, 2)
self.assertEqual(sr.items[0].rejected_warehouse, rejected_warehouse)
# Step 2: Create Subcontract Return from Rejected Warehouse
sr_return = make_subcontract_return_against_rejected_warehouse(sr.name)
# Verify the return document properties
self.assertEqual(sr_return.doctype, "Subcontracting Receipt")
self.assertEqual(sr_return.is_return, 1)
self.assertEqual(sr_return.return_against, sr.name)
# Verify item details in return document
self.assertEqual(len(sr_return.items), 1)
self.assertEqual(sr_return.items[0].item_code, fg_item)
self.assertEqual(sr_return.items[0].warehouse, rejected_warehouse)
self.assertEqual(sr_return.items[0].qty, -2.0) # Negative for return
self.assertEqual(sr_return.items[0].rejected_qty, 0.0)
self.assertEqual(sr_return.items[0].rejected_warehouse, "")
# Check specific fields that should be set for subcontracting returns
self.assertEqual(sr_return.items[0].subcontracting_order, sco.name)
self.assertEqual(sr_return.items[0].subcontracting_order_item, sr.items[0].subcontracting_order_item)
self.assertEqual(sr_return.items[0].return_qty_from_rejected_warehouse, 1)
# For returns from rejected warehouse, supplied_items might be empty initially
# They might get populated when the document is saved/submitted
# Or they might not be needed since we're returning finished goods
# Save and submit the return
sr_return.save()
sr_return.submit()
# Verify final state
sr_return.reload()
self.assertEqual(sr_return.docstatus, 1)
self.assertEqual(sr_return.status, "Return")
# Verify stock ledger entries for the return
sle = frappe.get_all(
"Stock Ledger Entry",
filters={
"voucher_type": "Subcontracting Receipt",
"voucher_no": sr_return.name,
"warehouse": rejected_warehouse,
},
fields=["item_code", "actual_qty", "warehouse"],
)
self.assertEqual(len(sle), 1)
self.assertEqual(sle[0].item_code, fg_item)
self.assertEqual(sle[0].actual_qty, -2.0) # Outward entry from rejected warehouse
self.assertEqual(sle[0].warehouse, rejected_warehouse)
# Verify that the original document's rejected quantity is not affected
sr.reload()
self.assertEqual(sr.items[0].rejected_qty, 2) # Should remain the same
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
def test_auto_create_purchase_receipt(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order

View File

@@ -484,10 +484,16 @@ def get_documents_with_active_service_level_agreement():
def set_documents_with_active_service_level_agreement():
active = frozenset(
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
)
frappe.cache.set_value("doctypes_with_active_sla", active)
try:
active = frozenset(
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
)
frappe.cache.set_value("doctypes_with_active_sla", active)
except (frappe.DoesNotExistError, frappe.db.TableMissingError):
# This happens during install / uninstall when wildcard hook for SLA intercepts some doc action.
# In both cases, the error can be safely ignored.
active = frozenset()
return active