Compare commits

..

124 Commits

Author SHA1 Message Date
Frappe PR Bot
1e16e751ee chore(release): Bumped to Version 15.95.0
# [15.95.0](https://github.com/frappe/erpnext/compare/v15.94.3...v15.95.0) (2026-01-20)

### Bug Fixes

* **accounts_controller:** make return message translatable ([8f6095d](8f6095d05f))
* **accounts:** add missing accounting dimensions in advance taxes and charges ([1d5f406](1d5f406930))
* add other charges in total ([3ef4fa5](3ef4fa51dc))
* allow disassemble stock entry without work order (backport [#51761](https://github.com/frappe/erpnext/issues/51761)) ([#51835](https://github.com/frappe/erpnext/issues/51835)) ([be20698](be2069883e))
* calculate net profit amount from root node accounts ([e9573b0](e9573b0b93))
* common_party_path ([#51826](https://github.com/frappe/erpnext/issues/51826)) ([6225217](62252170dd))
* docs_path ([b3df300](b3df300ea5))
* **manufacturing:** consider process loss qty while validating the work order ([4418fb4](4418fb48a9))
* **pos:** reapply set warehouse during cart update ([75b4a0a](75b4a0a89c))
* **postgres:** compute current month sales without DATE_FORMAT ([fbf4305](fbf4305028))
* **postgres:** fix v15 migration failures on Postgres ([#51481](https://github.com/frappe/erpnext/issues/51481)) ([eef26fe](eef26fea9a))
* prevent UOM from updating incorrectly while scanning barcode ([d196956](d196956307))
* **process statement of accounts:** allow renaming ([8b2778b](8b2778b29f))
* **process statement of accounts:** naming of reports ([054468a](054468a5ef))
* RFQ does not fetch html response ([90e8090](90e8090dcc))
* **sales analytics:** add curve filter ([c2995f6](c2995f6800))
* Show non-SLE vouchers with GL entries in Stock vs Account Value Comparison report ([6219d7d](6219d7d9a5))
* **stock entry:** calculate transferred quantity using transfer_qty (backport [#51656](https://github.com/frappe/erpnext/issues/51656)) ([#51675](https://github.com/frappe/erpnext/issues/51675)) ([1da781f](1da781f2ae))
* **stock:** resolve quantity issue when adding items via barcode scan ([c508ef5](c508ef5b82))
* **transaction.js:** use flt instead of cint for plc_conversion_rate ([f618bf2](f618bf212f))
* valuation rate for non batchwise valuation ([3008c7a](3008c7ad82))

### Features

* add new 2025 Charts of Accounts for France ([6af6fe8](6af6fe8204))
* **process statement of accounts:** added more frequency options for auto email ([546ab05](546ab05eb5))
* remove old French chart of accounts with code as nex 2025 is provided ([e568ab2](e568ab2255))

### Performance Improvements

* prevent duplicate reposting for the same item ([eff9595](eff9595e34))
2026-01-20 16:40:54 +00:00
ruthra kumar
cff3407a4b Merge pull request #51912 from frappe/version-15-hotfix
chore: release v15
2026-01-20 22:09:26 +05:30
mergify[bot]
502a262637 Merge pull request #51935 from frappe/mergify/bp/version-15-hotfix/pr-51934
fix: validation message in stock reco row idx (backport #51934)
2026-01-20 16:11:53 +00:00
rohitwaghchaure
8f112c5967 Merge pull request #51931 from frappe/mergify/bp/version-15-hotfix/pr-51930
Revert "perf: prevent duplicate reposting for the same item" (backport #51930)
2026-01-20 20:05:31 +05:30
rohitwaghchaure
dad7657853 Revert "perf: prevent duplicate reposting for the same item"
(cherry picked from commit 6e4b90055f)
2026-01-20 14:19:24 +00:00
rohitwaghchaure
ff84edcfad Merge pull request #51923 from frappe/mergify/bp/version-15-hotfix/pr-51920
perf: prevent duplicate reposting for the same item (backport #51920)
2026-01-20 18:01:07 +05:30
Rohit Waghchaure
eff9595e34 perf: prevent duplicate reposting for the same item
(cherry picked from commit 7535931571)
2026-01-20 12:08:49 +00:00
ruthra kumar
8847e1c2bd Merge pull request #51915 from frappe/mergify/bp/version-15-hotfix/pr-51671
fix(accounts): add missing accounting dimensions in advance taxes and charges (backport #51671)
2026-01-20 17:21:07 +05:30
Nikhil Kothari
1d5f406930 fix(accounts): add missing accounting dimensions in advance taxes and charges
(cherry picked from commit 22e9cb4cf4)

# Conflicts:
#	erpnext/patches.txt
2026-01-20 17:03:46 +05:30
Mihir Kandoi
091272409e Merge pull request #51674 from aerele/v15-sales-analytics-curve-filter 2026-01-20 13:26:31 +05:30
ravibharathi656
c2995f6800 fix(sales analytics): add curve filter 2026-01-20 13:01:59 +05:30
ruthra kumar
39cd371fb6 Merge pull request #51892 from frappe/mergify/bp/version-15-hotfix/pr-51886
fix(accounts_controller): make return message translatable (backport #51886)
2026-01-20 08:30:46 +05:30
barredterra
8f6095d05f fix(accounts_controller): make return message translatable
(cherry picked from commit 0209f0fe29)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2026-01-20 08:17:09 +05:30
ruthra kumar
6b61eabf61 Merge pull request #51883 from frappe/mergify/bp/version-15-hotfix/pr-51830
fix(manufacturing): consider process loss qty while validating the work order (backport #51830)
2026-01-20 08:08:43 +05:30
ruthra kumar
25112468bc Merge pull request #51890 from frappe/mergify/bp/version-15-hotfix/pr-51561
fix: delete advance ledger entries  while reconciling payment entry (backport #51561)
2026-01-20 08:06:13 +05:30
Lakshit Jain
d27fe6f57a Merge pull request #51561 from ljain112/fic-adv-ple-po
fix: delete advance ledger entries  while reconciling payment entry
(cherry picked from commit aea70c5ec1)
2026-01-20 02:21:23 +00:00
Diptanil Saha
e60064f6f1 Merge pull request #51885 from frappe/mergify/bp/version-15-hotfix/pr-49957
fix: process statement of accounts (backport #49957)
2026-01-19 22:19:58 +05:30
diptanilsaha
e621a51225 chore: resolve conflicts 2026-01-19 22:03:01 +05:30
diptanilsaha
054468a5ef fix(process statement of accounts): naming of reports
(cherry picked from commit 4a4c2188ec)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
2026-01-19 16:23:43 +00:00
diptanilsaha
546ab05eb5 feat(process statement of accounts): added more frequency options for auto email
(cherry picked from commit d610d1dccd)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
2026-01-19 16:23:42 +00:00
diptanilsaha
8b2778b29f fix(process statement of accounts): allow renaming
(cherry picked from commit dbab718aaa)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
2026-01-19 16:23:42 +00:00
Sudharsanan11
4418fb48a9 fix(manufacturing): consider process loss qty while validating the work order
(cherry picked from commit e6366e830c)
2026-01-19 16:18:35 +00:00
Diptanil Saha
cfafd39543 Merge pull request #51876 from frappe/mergify/bp/version-15-hotfix/pr-51595 2026-01-19 18:07:22 +05:30
Florian HENRY
794d005923 chore: re add older template
(cherry picked from commit b3efb3084f)
2026-01-19 12:23:31 +00:00
Florian HENRY
da19761fbd chore: fix bank account type
(cherry picked from commit 4fe1b214c1)
2026-01-19 12:23:31 +00:00
Florian HENRY
5fcda5f3ed chore: fix CASH acount type
(cherry picked from commit 6a876de838)
2026-01-19 12:23:31 +00:00
Florian HENRY
763cf6ae10 chore: fix bank acount type
(cherry picked from commit 765487a087)
2026-01-19 12:23:30 +00:00
Florian HENRY
b301be1a74 chore: add Expenses Included In Valuation account
(cherry picked from commit c519cd0268)
2026-01-19 12:23:30 +00:00
Florian HENRY
e568ab2255 feat: remove old French chart of accounts with code as nex 2025 is provided
(cherry picked from commit bf430fce09)
2026-01-19 12:23:30 +00:00
Florian HENRY
61295e7d47 chore: Review PR #51595
(cherry picked from commit 6bdaeb983d)
2026-01-19 12:23:30 +00:00
Florian HENRY
6af6fe8204 feat: add new 2025 Charts of Accounts for France
(cherry picked from commit c81dee137f)
2026-01-19 12:23:30 +00:00
rohitwaghchaure
d0d776486e Merge pull request #51865 from frappe/mergify/bp/version-15-hotfix/pr-51769
fix(pos): reapply set warehouse during cart update (backport #51769)
2026-01-19 15:44:52 +05:30
ravibharathi656
75b4a0a89c fix(pos): reapply set warehouse during cart update
(cherry picked from commit 5a53c45321)
2026-01-19 10:07:47 +00:00
ruthra kumar
a2c86dbe01 Merge pull request #51838 from frappe/mergify/bp/version-15-hotfix/pr-51787
fix: recalculate taxes when item tax template changes after discount (backport #51787)
2026-01-19 14:36:59 +05:30
ljain112
2bf75a2c24 chore: resolve conflicts 2026-01-19 13:41:40 +05:30
ruthra kumar
2b38bc191e Merge pull request #51843 from frappe/mergify/bp/version-15-hotfix/pr-51826
fix: common_party_path (backport #51826)
2026-01-19 13:20:48 +05:30
mahsem
62252170dd fix: common_party_path (#51826)
* fix: common_pary_path

* chore: remove non-existent anchor

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 0c0f43f7f7)
2026-01-19 07:50:20 +00:00
ruthra kumar
c55512c9d3 Merge pull request #51840 from frappe/mergify/bp/version-15-hotfix/pr-51513
fix: calculate net profit amount from root node accounts (backport #51513)
2026-01-19 13:06:17 +05:30
mergify[bot]
be2069883e fix: allow disassemble stock entry without work order (backport #51761) (#51835)
* fix: allow disassemble stock entry without work order (#51761)

* fix: allow disassemble stock entry without work order

* fix: use existing functionality to load fg item

* chore: better dict update

(cherry picked from commit 83919119f8)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py

* chore: fix conflicts

Removed unused test functions related to stock entry and sample retention.

* chore: fix linters issue

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-01-19 12:55:03 +05:30
Navin-S-R
e9573b0b93 fix: calculate net profit amount from root node accounts
(cherry picked from commit c84986d00e)
2026-01-19 07:15:09 +00:00
Lakshit Jain
1d64373c26 Merge pull request #51787 from ljain112/fix-taxes-disc
fix: recalculate taxes when item tax template changes after discount
(cherry picked from commit f00aeec9b4)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2026-01-19 07:01:36 +00:00
ruthra kumar
6df80901b9 Merge pull request #51833 from frappe/mergify/bp/version-15-hotfix/pr-51742
fix: add other charges in total (backport #51742)
2026-01-19 11:34:12 +05:30
SowmyaArunachalam
3ef4fa51dc fix: add other charges in total
(cherry picked from commit 9406c07c42)
2026-01-19 05:45:16 +00:00
Mihir Kandoi
cce32507d9 Merge pull request #51820 from frappe/mergify/bp/version-15-hotfix/pr-51817
fix: prevent UOM from updating incorrectly while scanning barcode (backport #51817)
2026-01-18 15:11:05 +05:30
Pandiyan5273
d196956307 fix: prevent UOM from updating incorrectly while scanning barcode
(cherry picked from commit 30263b26a5)
2026-01-18 09:36:26 +00:00
ruthra kumar
4db62cab3b Merge pull request #51796 from frappe/mergify/bp/version-15-hotfix/pr-51555
fix(postgres): compute current month sales without DATE_FORMAT (backport #51555)
2026-01-16 17:15:41 +05:30
Matt Howard
fbf4305028 fix(postgres): compute current month sales without DATE_FORMAT
(cherry picked from commit 64f391adf7)
2026-01-16 11:28:48 +00:00
ili.ad
eef26fea9a fix(postgres): fix v15 migration failures on Postgres (#51481)
* fix(postgres): avoid DISTINCT(...) in repost allowed types query

* fix(postgres): rewrite update pick list patch to avoid UPDATE JOIN

* chore: linting changes

---------

Co-authored-by: Matt Howard <github.severity519@passmail.net>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-01-16 16:35:16 +05:30
Mihir Kandoi
043d208580 Merge pull request #51793 from frappe/mergify/bp/version-15-hotfix/pr-51790
fix(stock): resolve quantity issue when adding items via barcode scan (backport #51790)
2026-01-16 16:20:18 +05:30
Pandiyan5273
c508ef5b82 fix(stock): resolve quantity issue when adding items via barcode scan
(cherry picked from commit f959b2c59a)
2026-01-16 10:48:53 +00:00
mergify[bot]
1da781f2ae fix(stock entry): calculate transferred quantity using transfer_qty (backport #51656) (#51675)
* fix(stock entry): calculate transferred quantity using transfer_qty

(cherry picked from commit 4e6d86d6f0)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py

* test: allow from_warehouse while creating material request

(cherry picked from commit 7e99148357)

* test: validate transferred quantity for material transfer entry

(cherry picked from commit bf2ab32abf)

* chore: fix conflicts

---------

Co-authored-by: Navin-S-R <navin@aerele.in>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-01-16 10:42:01 +05:30
rohitwaghchaure
e2b53884fe Merge pull request #51771 from frappe/mergify/bp/version-15-hotfix/pr-51768
fix: Show non-SLE vouchers with GL entries in Stock vs Account Value … (backport #51768)
2026-01-15 20:59:48 +05:30
rohitwaghchaure
de46ac8b62 chore: fix conflicts 2026-01-15 19:27:46 +05:30
Rohit Waghchaure
6219d7d9a5 fix: Show non-SLE vouchers with GL entries in Stock vs Account Value Comparison report
(cherry picked from commit 1db9ce205f)

# Conflicts:
#	erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
2026-01-15 12:20:31 +00:00
rohitwaghchaure
eb0249310c Merge pull request #51751 from frappe/mergify/bp/version-15-hotfix/pr-51729
fix: valuation rate for non batchwise valuation (backport #51729)
2026-01-15 17:01:48 +05:30
Mihir Kandoi
dfb1722dc4 Merge pull request #51766 from aerele/rfq-email-refactor 2026-01-15 16:38:15 +05:30
Sudharsanan Ashok
c13f3ba695 Merge branch 'version-15-hotfix' into rfq-email-refactor 2026-01-15 15:36:20 +05:30
Mihir Kandoi
add635b9eb refactor: backport RFQ email refactor (#51503) 2026-01-15 15:33:03 +05:30
Mihir Kandoi
28a670434d Merge pull request #51763 from frappe/mergify/bp/version-15-hotfix/pr-51364
fix: RFQ does not fetch html response (backport #51364)
2026-01-15 12:12:59 +05:30
Mihir Kandoi
90e8090dcc fix: RFQ does not fetch html response
(cherry picked from commit da899913b8)
2026-01-15 06:17:42 +00:00
Mihir Kandoi
d983280de8 Merge pull request #51754 from frappe/mergify/bp/version-15-hotfix/pr-51753
fix: docs_path (backport #51753)
2026-01-14 21:30:54 +05:30
mahsem
b3df300ea5 fix: docs_path
(cherry picked from commit 7ef8c81caf)
2026-01-14 15:59:59 +00:00
rohitwaghchaure
0a363f879d chore: fix conflicts
Removed multiple test cases related to purchase receipts and negative stock handling.
2026-01-14 19:43:39 +05:30
Rohit Waghchaure
3008c7ad82 fix: valuation rate for non batchwise valuation
(cherry picked from commit b6312bca9c)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2026-01-14 14:06:35 +00:00
Frappe PR Bot
d82ab066bd chore(release): Bumped to Version 15.94.3
## [15.94.3](https://github.com/frappe/erpnext/compare/v15.94.2...v15.94.3) (2026-01-14)

### Bug Fixes

* **transaction.js:** use flt instead of cint for plc_conversion_rate ([9819ed1](9819ed112b))
2026-01-14 12:17:05 +00:00
Diptanil Saha
df46841f82 Merge pull request #51749 from frappe/mergify/bp/version-15/pr-51747
fix(transaction.js): use flt instead of cint for plc_conversion_rate (backport #51730) (backport #51747)
2026-01-14 17:45:40 +05:30
Diptanil Saha
14d197d9eb chore: resolve conflict
(cherry picked from commit d5982cab03)
2026-01-14 12:12:56 +00:00
diptanilsaha
9819ed112b fix(transaction.js): use flt instead of cint for plc_conversion_rate
(cherry picked from commit 8b445e04e5)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
(cherry picked from commit f618bf212f)
2026-01-14 12:12:55 +00:00
Diptanil Saha
85f635ac4a Merge pull request #51747 from frappe/mergify/bp/version-15-hotfix/pr-51730
fix(transaction.js): use flt instead of cint for plc_conversion_rate (backport #51730)
2026-01-14 15:56:48 +05:30
Diptanil Saha
d5982cab03 chore: resolve conflict 2026-01-14 15:54:10 +05:30
diptanilsaha
f618bf212f fix(transaction.js): use flt instead of cint for plc_conversion_rate
(cherry picked from commit 8b445e04e5)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2026-01-14 10:22:15 +00:00
Frappe PR Bot
ddca3b5800 chore(release): Bumped to Version 15.94.2
## [15.94.2](https://github.com/frappe/erpnext/compare/v15.94.1...v15.94.2) (2026-01-13)

### Bug Fixes

* **accounting-dimension:** System-generated round-off GL entries fail to set the accounting dimension ([#51167](https://github.com/frappe/erpnext/issues/51167)) ([1179514](1179514118))
* **accounts:** correct sales order item deletion message for MR and PO linkage ([4c53af0](4c53af0494))
* allow all users of supplier to create purchase invoices ([8f1509d](8f1509dca1))
* **asset value adjustment:** skip cancelling revaluation journal entry if already cancelled ([dae6adf](dae6adfe13))
* **asset:** properly reset purchase reference and item fields ([ea0b768](ea0b76831f))
* **asset:** remove references for composite and existing asset ([c7f79d1](c7f79d16e9))
* change float types in payment entry reference table to currency ([d17deba](d17debabf7))
* closed WO becomes open when RM is returned ([7db6ae8](7db6ae8bda))
* correct uom reflecting in sales order when fetching from barcode ([3cc41cf](3cc41cf643))
* don't duplicate default income account to Item ([#50413](https://github.com/frappe/erpnext/issues/50413)) ([1cb22f9](1cb22f9d05)), closes [#48231](https://github.com/frappe/erpnext/issues/48231)
* ignore permissions when cancelling revaluation journal entry ([129457b](129457b2ce))
* incoming rate calculation ([01af6c8](01af6c8762))
* **minor:** hide target_qty field from the capitalization ([ed05b4c](ed05b4cc5c))
* move validation to before_cancel ([11d23e1](11d23e1a4a))
* negative stock issue for higher precision ([1bbeecf](1bbeecff12))
* **payment reconciliation:** handle adhoc payment returns ([#51311](https://github.com/frappe/erpnext/issues/51311)) ([159d1d6](159d1d61b5))
* pick list qty does not reset when pick list is cancelled ([f9be364](f9be364bd1))
* prevent manual cancellation of the linked Revaluation Journal Entry ([07de3f4](07de3f4391))
* remove posting date & time on SRE batch validation ([d3f2da0](d3f2da0d59))
* **stock:** enable allow on submit for tracking status field ([9d5a493](9d5a493609))

### Performance Improvements

* SABB taking time to save the record ([ee9debe](ee9debe581))
2026-01-13 15:02:59 +00:00
ruthra kumar
a8dbf981d8 Merge pull request #51711 from frappe/version-15-hotfix
chore: release v15
2026-01-13 20:31:27 +05:30
Khushi Rawat
b807f9318f Merge pull request #51721 from khushi8112/hide-target-qty-field
fix: hide target_qty field from the capitalization
2026-01-13 17:46:29 +05:30
khushi8112
a66129af29 chore: run pre-commit 2026-01-13 17:28:51 +05:30
khushi8112
ed05b4cc5c fix(minor): hide target_qty field from the capitalization 2026-01-13 17:26:32 +05:30
Khushi Rawat
00ac931722 Merge pull request #51717 from frappe/mergify/bp/version-15-hotfix/pr-51666
fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled (backport #51666)
2026-01-13 16:54:29 +05:30
khushi8112
3365bc3ba3 chore: rebase with v15 branch 2026-01-13 16:23:00 +05:30
Navin-S-R
11d23e1a4a fix: move validation to before_cancel
(cherry picked from commit d65cd605a1)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.py
2026-01-13 10:38:03 +00:00
Navin-S-R
07de3f4391 fix: prevent manual cancellation of the linked Revaluation Journal Entry
(cherry picked from commit 73b038084b)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.py
2026-01-13 10:38:03 +00:00
Navin-S-R
129457b2ce fix: ignore permissions when cancelling revaluation journal entry
(cherry picked from commit 500c44e3f5)
2026-01-13 10:38:03 +00:00
Navin-S-R
426516a1ee refactor(journal entry): replace raw SQL with query builder to unlink asset value adjustment
(cherry picked from commit 5f00239bba)
2026-01-13 10:38:02 +00:00
Navin-S-R
dae6adfe13 fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled
(cherry picked from commit b1704ccef1)

# Conflicts:
#	erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
2026-01-13 10:38:02 +00:00
Mihir Kandoi
f5cae2d60b Merge pull request #51657 from mihir-kandoi/v16 2026-01-13 10:55:15 +05:30
Mihir Kandoi
4e94e3726c chore: grammar fix 2026-01-13 10:50:15 +05:30
Mihir Kandoi
d7bf1a179a chore: update links 2026-01-13 10:43:34 +05:30
ruthra kumar
6632f3d446 Merge pull request #51332 from frappe/mergify/bp/version-15-hotfix/pr-50413
fix: don't duplicate default income account to Item (backport #50413)
2026-01-12 20:33:41 +05:30
Khushi Rawat
b909ec9388 Merge pull request #51686 from frappe/mergify/bp/version-15-hotfix/pr-51678
fix(asset): properly reset purchase reference and item fields (backport #51678)
2026-01-12 15:54:38 +05:30
khushi8112
ea0b76831f fix(asset): properly reset purchase reference and item fields
(cherry picked from commit 671610db1e)
2026-01-12 10:17:24 +00:00
Khushi Rawat
57c759dfcd Merge pull request #51677 from frappe/mergify/bp/version-15-hotfix/pr-51630
fix(asset): remove references  for composite and existing assets (backport #51630)
2026-01-12 13:03:47 +05:30
nivithamerlin
c7f79d16e9 fix(asset): remove references for composite and existing asset
(cherry picked from commit c1d50c492b)
2026-01-12 07:30:51 +00:00
ruthra kumar
940cfb58a7 Merge pull request #51665 from frappe/mergify/bp/version-15-hotfix/pr-51311
fix(payment reconciliation): handle adhoc payment returns (backport #51311)
2026-01-11 19:37:47 +05:30
NaviN
159d1d61b5 fix(payment reconciliation): handle adhoc payment returns (#51311)
* fix(payment reconciliation): handle reverse payments

* test: validate payment return gain or loss

* chore: typo

(cherry picked from commit cecd07bbf4)
2026-01-11 13:28:36 +00:00
Mihir Kandoi
d8232c4503 chore: v16 release announcement for v15 users 2026-01-10 22:22:10 +05:30
Mihir Kandoi
ae72b99846 Merge pull request #51654 from frappe/mergify/bp/version-15-hotfix/pr-51652 2026-01-10 18:27:48 +05:30
Mihir Kandoi
f9be364bd1 fix: pick list qty does not reset when pick list is cancelled
(cherry picked from commit 1d6d9c2040)
2026-01-10 12:44:16 +00:00
rohitwaghchaure
7ac55379ec Merge pull request #51632 from frappe/mergify/bp/version-15-hotfix/pr-51351
perf: SABB taking time to save the record (backport #51351)
2026-01-09 18:50:35 +05:30
Rohit Waghchaure
aa43715de6 chore: fix conflicts 2026-01-09 18:16:38 +05:30
Rohit Waghchaure
01af6c8762 fix: incoming rate calculation
(cherry picked from commit 8e143d68b4)
2026-01-09 12:17:32 +00:00
Rohit Waghchaure
ee9debe581 perf: SABB taking time to save the record
(cherry picked from commit 20320c4a6c)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
#	erpnext/stock/serial_batch_bundle.py
2026-01-09 12:17:32 +00:00
mergify[bot]
d83365734e Merge pull request #51624 from frappe/mergify/bp/version-15-hotfix/pr-50869
fix: do cancellation procedures on WO close (backport #50869)
2026-01-09 09:48:28 +00:00
ruthra kumar
1fb554c312 Merge pull request #51598 from frappe/mergify/bp/version-15-hotfix/pr-51534
fix(accounts): correct sales order item deletion message for MR and PO linkage (backport #51534)
2026-01-08 17:57:30 +05:30
Pandiyan5273
4c53af0494 fix(accounts): correct sales order item deletion message for MR and PO linkage
(cherry picked from commit 5a47503611)
2026-01-08 12:10:25 +00:00
rohitwaghchaure
e9c14e88df Merge pull request #51597 from frappe/mergify/bp/version-15-hotfix/pr-51574
fix(stock): enable allow on submit for tracking status field (backport #51574)
2026-01-08 16:50:29 +05:30
rohitwaghchaure
e6dbd06435 Merge pull request #51533 from nishkagosalia/gh-51381
fix: correct uom reflecting in sales order when fetching from..
2026-01-08 16:31:37 +05:30
Pandiyan5273
9d5a493609 fix(stock): enable allow on submit for tracking status field
(cherry picked from commit 1bfb62465f)
2026-01-08 11:01:03 +00:00
rohitwaghchaure
530c0b0bd6 Merge pull request #51588 from frappe/mergify/bp/version-15-hotfix/pr-51586
fix: negative stock issue for higher precision (backport #51586)
2026-01-08 15:03:48 +05:30
rohitwaghchaure
5193dbba9b chore: fix conflicts
Refactor test cases for delivery notes to handle negative stock and higher precision.
2026-01-08 14:45:39 +05:30
Mihir Kandoi
42658f7b1c Merge pull request #51587 from frappe/mergify/bp/version-15-hotfix/pr-51585
fix: closed WO becomes open when RM is returned (backport #51585)
2026-01-08 14:38:34 +05:30
Rohit Waghchaure
1bbeecff12 fix: negative stock issue for higher precision
(cherry picked from commit 87be020c78)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2026-01-08 09:07:10 +00:00
Mihir Kandoi
7db6ae8bda fix: closed WO becomes open when RM is returned
(cherry picked from commit d0ba365aaa)
2026-01-08 08:53:28 +00:00
Mihir Kandoi
2bdd14c831 Merge pull request #51584 from frappe/mergify/bp/version-15-hotfix/pr-51583
fix: allow all users of supplier to create purchase invoices (backport #51583)
2026-01-08 13:47:24 +05:30
Frappe PR Bot
c805c7fac4 chore(release): Bumped to Version 15.94.1
## [15.94.1](https://github.com/frappe/erpnext/compare/v15.94.0...v15.94.1) (2026-01-08)

### Bug Fixes

* remove posting date & time on SRE batch validation ([69259c9](69259c9933))
2026-01-08 08:16:29 +00:00
rohitwaghchaure
7238636766 Merge pull request #51578 from frappe/mergify/bp/version-15/pr-51553
fix: remove posting date & time on SRE batch validation (backport #51553)
2026-01-08 13:45:06 +05:30
Mihir Kandoi
8f1509dca1 fix: allow all users of supplier to create purchase invoices
(cherry picked from commit 190204a939)
2026-01-08 08:02:13 +00:00
ruthra kumar
a7f59fece3 Merge pull request #51580 from frappe/mergify/bp/version-15-hotfix/pr-51167
fix(accounting-dimension): System-generated round-off GL entries fail to set the accounting dimension (backport #51167)
2026-01-08 12:21:21 +05:30
Logesh Periyasamy
1179514118 fix(accounting-dimension): System-generated round-off GL entries fail to set the accounting dimension (#51167)
* chore: remove disabled condition statement

* fix: add default dimension for round off gle

* fix: validate report type to handle opening entries roundoff

(cherry picked from commit bc63c85daf)
2026-01-08 06:36:27 +00:00
kavin-114
69259c9933 fix: remove posting date & time on SRE batch validation
(cherry picked from commit d3f2da0d59)
2026-01-08 05:51:19 +00:00
rohitwaghchaure
7aee6bdaf8 Merge pull request #51553 from aerele/support-52652
fix: remove posting date & time on SRE batch validation
2026-01-07 11:41:32 +05:30
ruthra kumar
f11fb0e45f Merge pull request #51549 from frappe/mergify/bp/version-15-hotfix/pr-51528
fix: change float types in payment entry reference table to currency (backport #51528)
2026-01-07 11:39:37 +05:30
trustedcomputer
d17debabf7 fix: change float types in payment entry reference table to currency
(cherry picked from commit 8ba71300db)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
2026-01-07 11:24:25 +05:30
kavin-114
d3f2da0d59 fix: remove posting date & time on SRE batch validation 2026-01-07 01:00:29 +05:30
Nishka Gosalia
3cc41cf643 fix: correct uom reflecting in sales order when fetching from barcode 2026-01-06 12:51:47 +05:30
Raffael Meyer
1cb22f9d05 fix: don't duplicate default income account to Item (#50413)
* fix: don't duplicate default income account to Item

Only store _Default Income Account_ in **Item** if it's different from the **Company**'s  _Default Income Account_.

Resolves #48231

* refactor: move db call out of loop

* docs: add docstring

(cherry picked from commit b6cb9d4799)
2025-12-25 09:23:14 +00:00
65 changed files with 7371 additions and 569 deletions

View File

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

View File

@@ -307,7 +307,7 @@
},
{
"default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"

View File

@@ -187,7 +187,6 @@ class GLEntry(Document):
account_type == "Profit and Loss"
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
and not self.is_cancelled
):
if not self.get(dimension.fieldname):
@@ -201,7 +200,6 @@ class GLEntry(Document):
account_type == "Balance Sheet"
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
and not self.is_cancelled
):
if not self.get(dimension.fieldname):

View File

@@ -177,6 +177,9 @@ class JournalEntry(AccountsController):
else:
return self._submit()
def before_cancel(self):
self.has_asset_adjustment_entry()
def cancel(self):
if len(self.accounts) > 100:
queue_submission(self, "_cancel")
@@ -447,12 +450,27 @@ class JournalEntry(AccountsController):
)
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
def unlink_asset_adjustment_entry(self):
frappe.db.sql(
""" update `tabAsset Value Adjustment`
set journal_entry = null where journal_entry = %s""",
self.name,
def has_asset_adjustment_entry(self):
if self.flags.get("via_asset_value_adjustment"):
return
asset_value_adjustment = frappe.db.get_value(
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
)
if asset_value_adjustment:
frappe.throw(
_(
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
)
def unlink_asset_adjustment_entry(self):
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
(
frappe.qb.update(AssetValueAdjustment)
.set(AssetValueAdjustment.journal_entry, None)
.where(AssetValueAdjustment.journal_entry == self.name)
).run()
def validate_party(self):
for d in self.get("accounts"):

View File

@@ -68,7 +68,7 @@
{
"columns": 2,
"fieldname": "total_amount",
"fieldtype": "Float",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Grand Total",
"print_hide": 1,
@@ -77,7 +77,7 @@
{
"columns": 2,
"fieldname": "outstanding_amount",
"fieldtype": "Float",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding",
"read_only": 1
@@ -85,7 +85,7 @@
{
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Float",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated"
},
@@ -174,7 +174,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-25 04:32:11.040025",
"modified": "2026-01-05 14:18:03.286224",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -18,12 +18,12 @@ class PaymentEntryReference(Document):
account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
allocated_amount: DF.Float
allocated_amount: DF.Currency
bill_no: DF.Data | None
due_date: DF.Date | None
exchange_gain_loss: DF.Currency
exchange_rate: DF.Float
outstanding_amount: DF.Float
outstanding_amount: DF.Currency
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
@@ -34,7 +34,7 @@ class PaymentEntryReference(Document):
reconcile_effect_on: DF.Date | None
reference_doctype: DF.Link
reference_name: DF.DynamicLink
total_amount: DF.Float
total_amount: DF.Currency
# end: auto-generated types
@property

View File

@@ -133,7 +133,6 @@ class PaymentLedgerEntry(Document):
account_type == "Profit and Loss"
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(
@@ -146,7 +145,6 @@ class PaymentLedgerEntry(Document):
account_type == "Balance Sheet"
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion
from frappe.query_builder import Case, Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -393,6 +393,9 @@ class PaymentReconciliation(Document):
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
party_account_defaults = frappe.get_cached_value(
"Account", self.receivable_payable_account, ["account_type", "account_currency"], as_dict=True
)
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
@@ -400,9 +403,9 @@ class PaymentReconciliation(Document):
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
)
difference_amount = 0
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
) != frappe.get_cached_value("Company", self.company, "default_currency"):
if party_account_defaults.get("account_currency") != frappe.get_cached_value(
"Company", self.company, "default_currency"
):
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
@@ -414,7 +417,14 @@ class PaymentReconciliation(Document):
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
# Added If clause to handle return Adhoc payments for account type holders ("Payable")
if party_account_defaults.get("account_type") in ("Payable") and invoice.get(
"invoice_type"
) in ["Payment Entry", "Journal Entry"]:
difference_amount = allocated_amount_in_inv_rate - allocated_amount_in_ref_rate
else:
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
@@ -677,6 +687,28 @@ class PaymentReconciliation(Document):
)
invoice_exchange_map.update(journals_map)
payment_entries = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Payment Entry"
]
payment_entries.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Payment Entry"]
)
if payment_entries:
pe = frappe.qb.DocType("Payment Entry")
query = (
frappe.qb.from_(pe)
.select(
pe.name,
Case()
.when(pe.payment_type == "Receive", pe.source_exchange_rate)
.else_(pe.target_exchange_rate)
.as_("exchange_rate"),
)
.where(pe.name.isin(payment_entries))
)
payment_entries = query.run(as_list=1)
invoice_exchange_map.update(payment_entries)
return invoice_exchange_map
def validate_allocation(self):

View File

@@ -2336,6 +2336,210 @@ class TestPaymentReconciliation(FrappeTestCase):
frappe.db.set_value("Company", self.company, default_settings)
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
amount = 1000
exchange_rate_at_payment = 100
exchange_rate_at_reverse_payment = 95
# Receive amount from customer - 1,00,000
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer)
pe.payment_type = "Receive"
pe.paid_from = self.debtors_eur
pe.paid_from_account_currency = "EUR"
pe.source_exchange_rate = exchange_rate_at_payment
pe.paid_amount = amount
pe.received_amount = exchange_rate_at_payment * amount
pe.paid_to = self.cash
pe.paid_to_account_currency = "INR"
pe = pe.save().submit()
# Pay amount to customer - 95,000
reverse_pe = self.create_payment_entry(
amount=amount, posting_date=transaction_date, customer=customer
)
reverse_pe.payment_type = "Pay"
reverse_pe.paid_from = self.cash
reverse_pe.paid_from_account_currency = "INR"
reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment
reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount
reverse_pe.received_amount = amount
reverse_pe.paid_to = self.debtors_eur
reverse_pe.paid_to_account_currency = "EUR"
reverse_pe.save().submit()
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Check the difference_amount is a gain of 5000
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), 5000.0)
pr.reconcile()
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_supplier(self):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
amount = 1000
exchange_rate_at_payment = 100
exchange_rate_at_reverse_payment = 95
# Pay amount to supplier - 1,00,000
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
pe.payment_type = "Pay"
pe.party_type = "Supplier"
pe.party = self.supplier
pe.paid_from = self.cash
pe.paid_from_account_currency = "INR"
pe.target_exchange_rate = exchange_rate_at_payment
pe.paid_amount = exchange_rate_at_payment * amount
pe.received_amount = amount
pe.paid_to = self.creditors_usd
pe.paid_to_account_currency = "USD"
pe.save().submit()
# Receive amount from supplier - 95,000
reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
reverse_pe.payment_type = "Receive"
reverse_pe.party_type = "Supplier"
reverse_pe.party = self.supplier
reverse_pe.paid_from = self.creditors_usd
reverse_pe.paid_from_account_currency = "USD"
reverse_pe.source_exchange_rate = exchange_rate_at_reverse_payment
reverse_pe.paid_amount = amount
reverse_pe.received_amount = exchange_rate_at_reverse_payment * amount
reverse_pe.paid_to = self.cash
reverse_pe.paid_to_account_currency = "INR"
reverse_pe = reverse_pe.save().submit()
# Reconcile payments
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.party = self.supplier
pr.receivable_payable_account = self.creditors_usd
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Check the difference_amount is a loss of 5000
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0)
pr.reconcile()
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
amount = 1000
exchange_rate_at_payment = 95
exchange_rate_at_reverse_payment = 100
# Receive amount from customer - 95,000
je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date)
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount
je1.accounts[0].debit = exchange_rate_at_payment * amount
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = customer
je1.accounts[1].exchange_rate = exchange_rate_at_payment
je1.accounts[1].credit_in_account_currency = amount
je1.accounts[1].credit = exchange_rate_at_payment * amount
je1.save()
je1.submit()
# Pay amount to customer - 1,00,000
je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date)
je2.multi_currency = 1
je2.accounts[0].party_type = "Customer"
je2.accounts[0].party = customer
je2.accounts[0].exchange_rate = exchange_rate_at_reverse_payment
je2.accounts[0].debit_in_account_currency = amount
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
je2.accounts[1].exchange_rate = 1
je2.accounts[1].credit_in_account_currency = exchange_rate_at_reverse_payment * amount
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
je2.save()
je2.submit()
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Check the difference_amount is a loss of 5000
self.assertEqual(flt(pr.allocation[0].difference_amount), -5000.0)
pr.reconcile()
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_supplier(self):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
amount = 1000
exchange_rate_at_payment = 95
exchange_rate_at_reverse_payment = 100
# Pay amount to supplier - 95,000
je1 = self.create_journal_entry(self.creditors_usd, self.cash, amount, transaction_date)
je1.multi_currency = 1
je1.accounts[0].party_type = "Supplier"
je1.accounts[0].party = self.supplier
je1.accounts[0].exchange_rate = exchange_rate_at_payment
je1.accounts[0].debit_in_account_currency = amount
je1.accounts[0].debit = exchange_rate_at_payment * amount
je1.accounts[1].exchange_rate = 1
je1.accounts[1].credit = exchange_rate_at_payment * amount
je1.accounts[1].credit_in_account_currency = exchange_rate_at_payment * amount
je1.save()
je1.submit()
# Receive amount from supplier - 1,00,000
je2 = self.create_journal_entry(self.cash, self.creditors_usd, amount, transaction_date)
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
je2.accounts[0].debit_in_account_currency = exchange_rate_at_reverse_payment * amount
je2.accounts[1].party_type = "Supplier"
je2.accounts[1].party = self.supplier
je2.accounts[1].exchange_rate = exchange_rate_at_reverse_payment
je2.accounts[1].credit_in_account_currency = amount
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
je2.save()
je2.submit()
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = self.creditors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Check the difference_amount is a gain of 5000
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
pr.reconcile()
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -481,6 +481,7 @@ class TestPOSInvoice(unittest.TestCase):
rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1,
ignore_sabb_validation=True,
)
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
@@ -956,6 +957,7 @@ class TestPOSInvoice(unittest.TestCase):
qty=1,
rate=100,
do_not_submit=True,
ignore_sabb_validation=True,
)
self.assertRaises(frappe.ValidationError, pos_inv.submit)
@@ -1097,6 +1099,7 @@ def create_pos_invoice(**args):
"posting_time": pos_inv.posting_time,
"type_of_transaction": type_of_transaction,
"do_not_submit": True,
"ignore_sabb_validation": args.ignore_sabb_validation,
}
)
).name

View File

@@ -13,7 +13,7 @@
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
@@ -67,7 +68,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Frequency",
"options": "Weekly\nMonthly\nQuarterly"
"options": "Daily\nWeekly\nBiweekly\nMonthly\nQuarterly"
},
{
"fieldname": "company",
@@ -401,7 +402,7 @@
}
],
"links": [],
"modified": "2025-08-04 18:21:12.603623",
"modified": "2025-10-07 12:19:20.719898",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.utils import add_days, add_months, format_date, getdate, today
from frappe.utils import add_days, add_months, add_to_date, format_date, getdate, today
from frappe.utils.jinja import validate_template
from frappe.utils.pdf import get_pdf
from frappe.www.printview import get_print_style
@@ -55,7 +55,7 @@ class ProcessStatementOfAccounts(Document):
enable_auto_email: DF.Check
filter_duration: DF.Int
finance_book: DF.Link | None
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
frequency: DF.Literal["Daily", "Weekly", "Biweekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check
@@ -529,8 +529,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
if doc.enable_auto_email and from_scheduler:
new_to_date = getdate(posting_date or today())
if doc.frequency == "Weekly":
new_to_date = add_days(new_to_date, 7)
if doc.frequency in ("Daily", "Weekly", "Biweekly"):
frequency = {"Daily": 1, "Weekly": 7, "Biweekly": 14}
new_to_date = add_days(new_to_date, frequency[doc.frequency])
else:
new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3)
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)

View File

@@ -6,228 +6,304 @@
.print-format td {
vertical-align:middle !important;
}
</style>
</style>
<div id="header-html" class="hidden-pdf">
{% if letter_head.content %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
</div>
<div id="footer-html" class="visible-pdf">
{% if letter_head.footer %}
<div class="letter-head-footer">
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
{{ letter_head.footer }}
</div>
{% endif %}
</div>
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
<h4 class="text-center">
{{ filters.customer_name }}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) %}
{{ _("Tax Id: ") }}{{ filters.tax_id }}
{% endif %}
</h6>
<h5 class="text-center">
{{ _(filters.ageing_based_on) }}
{{ _("Until") }}
{{ frappe.format(filters.report_date, 'Date') }}
</h5>
<div class="clearfix">
<div class="pull-left">
{% if(filters.payment_terms) %}
<strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
{% endif %}
</div>
<div class="pull-right">
{% if(filters.credit_limit) %}
<strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
{% endif %}
</div>
</div>
{% if(filters.show_future_payments) %}
{% set balance_row = data.slice(-1).pop() %}
{% for i in report.columns %}
{% if i.fieldname == 'age' %}
{% set elem = i %}
{% endif %}
{% endfor %}
{% set start = report.columns.findIndex(elem) %}
{% set range1 = report.columns[start].label %}
{% set range2 = report.columns[start+1].label %}
{% set range3 = report.columns[start+2].label %}
{% set range4 = report.columns[start+3].label %}
{% set range5 = report.columns[start+4].label %}
{% set range6 = report.columns[start+5].label %}
{% if(balance_row) %}
<table class="table table-bordered table-condensed">
<caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
<colgroup>
<col style="width: 30mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
</colgroup>
<thead>
<tr>
<th>{{ _(" ") }}</th>
<th>{{ _(range1) }}</th>
<th>{{ _(range2) }}</th>
<th>{{ _(range3) }}</th>
<th>{{ _(range4) }}</th>
<th>{{ _(range5) }}</th>
<th>{{ _(range6) }}</th>
<th>{{ _("Total") }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ _("Total Outstanding") }}</td>
<td class="text-right">
{{ format_number(balance_row["age"], null, 2) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
</td>
</tr>
<td>{{ _("Future Payments") }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
</td>
<tr class="cvs-footer">
<th class="text-left">{{ _("Cheques Required") }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
</tr>
</tbody>
</table>
{% endif %}
<div id="header-html" class="hidden-pdf">
{% if letter_head.content %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
<table class="table table-bordered">
</div>
<div id="footer-html" class="visible-pdf">
{% if letter_head.footer %}
<div class="letter-head-footer">
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
{{ letter_head.footer }}
</div>
{% endif %}
</div>
<h2 class="text-center" style="margin-top:0">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<h4 class="text-center">
{{ filters.customer_name }}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) %}
{{ _("Tax Id: {0}").format(filters.tax_id) }}
{% endif %}
</h6>
<h5 class="text-center">
{{ _("{0} until {1}").format(
_(filters.ageing_based_on),
frappe.format(filters.report_date, 'Date')
) }}
</h5>
<div class="clearfix">
<div class="pull-left">
{% if(filters.payment_terms) %}
<strong>{{ _("Payment Terms:") }}</strong> {{ filters.payment_terms }}
{% endif %}
</div>
<div class="pull-right">
{% if(filters.credit_limit) %}
<strong>{{ _("Credit Limit:") }}</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
{% endif %}
</div>
</div>
{% if(filters.show_future_payments)%}
{% set balance_row = data[-1] %}
{% set ns = namespace(idx=None) %}
{% for i in report.columns %}
{% if i.fieldname == "age" and ns.idx is none %}
{% set ns.idx = loop.index0 %}
{% endif %}
{% endfor %}
{% set age = report.columns[ns.idx].label %}
{% set range1 = report.columns[ns.idx+1].label %}
{% set range2 = report.columns[ns.idx+2].label %}
{% set range3 = report.columns[ns.idx+3].label %}
{% set range4 = report.columns[ns.idx+4].label %}
{% set range5 = report.columns[ns.idx+5].label %}
{% if(balance_row) %}
<table class="table table-bordered table-condensed">
<caption class="text-right">{{ _("Amount in {0}").format(data[0]["currency"] ~ "") }}</caption>
<colgroup>
<col style="width: 30mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
</colgroup>
<thead>
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
<th style="width: 10%">{{ _("Date") }}</th>
<th style="width: 4%">{{ _("Age (Days)") }}</th>
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<th style="width: 14%">{{ _("Reference") }}</th>
<th style="width: 10%">{{ _("Sales Person") }}</th>
{% else %}
<th style="width: 24%">{{ _("Reference") }}</th>
{% endif %}
{% if not(filters.show_future_payments) %}
<th style="width: 20%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks") }}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
{% if not(filters.show_future_payments) %}
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
<th style="width: 10%; text-align: right">
{% if report.report_name == "Accounts Receivable" %}
{{ _('Credit Note') }}
{% else %}
{{ _('Debit Note') }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
{% endif %}
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
{% endif %}
{% else %}
<th style="width: 40%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks")}}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
<th style="width: 15%">
{% if report.report_name == "Accounts Receivable Summary" %}
{{ _('Credit Note Amount') }}
{% else %}
{{ _('Debit Note Amount') }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
{% endif %}
<th>{{ _(" ") }}</th>
<th>{{ _(age) }}</th>
<th>{{ _(range1) }}</th>
<th>{{ _(range2) }}</th>
<th>{{ _(range3) }}</th>
<th>{{ _(range4) }}</th>
<th>{{ _(range5) }}</th>
<th>{{ _("Total") }}</th>
</tr>
</thead>
<tbody>
{% for i in range(data|length) %}
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
{% if(data[i]["party"]) %}
<td>{{ frappe.format((data[i]["posting_date"]), 'Date') }}</td>
<td style="text-align: right">{{ data[i]["age"] }}</td>
<td>
{% if not(filters.show_future_payments) %}
{{ data[i]["voucher_type"] }}
<br>
{% endif %}
{{ data[i]["voucher_no"] }}
</td>
<tr>
<td>{{ _("Total Outstanding") }}</td>
<td class="text-right">
{{ frappe.utils.flt(balance_row["age"], 2) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range1"], currency=balance_row["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range2"], currency=balance_row["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range3"], currency=balance_row["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range4"], currency=balance_row["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range5"], currency=balance_row["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"]), currency=balance_row["currency"]) }}
</td>
</tr>
<td>{{ _("Future Payments") }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["future_amount"]), currency=balance_row["currency"]) }}
</td>
<tr class="cvs-footer">
<th class="text-left">{{ _("Cheques Required") }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"] - balance_row["future_amount"]), currency=balance_row["currency"]) }}</th>
</tr>
</tbody>
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td>{{ data[i]["sales_person"] }}</td>
</table>
{% endif %}
{% endif %}
<table class="table table-bordered">
<thead>
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
<th style="width: 10%">{{ _("Date") }}</th>
<th style="width: 4%">{{ _("Age (Days)") }}</th>
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<th style="width: 14%">{{ _("Reference") }}</th>
<th style="width: 10%">{{ _("Sales Person") }}</th>
{% else %}
<th style="width: 24%">{{ _("Reference") }}</th>
{% endif %}
{% if not(filters.show_future_payments) and filters.show_remarks %}
<th style="width: 20%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks") }}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
{% if not(filters.show_future_payments) %}
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
<th style="width: 10%; text-align: right">
{% if report.report_name == "Accounts Receivable" %}
{{ _("Credit Note") }}
{% else %}
{{ _("Debit Note") }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
{% endif %}
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
{% endif %}
{% else %}
<th style="width: 40%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks")}}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
<th style="width: 15%">
{% if report.report_name == "Accounts Receivable Summary" %}
{{ _("Credit Note Amount") }}
{% else %}
{{ _("Debit Note Amount") }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for i in range(data|length) %}
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
{% if(data[i]["party"]) %}
<td>{{ frappe.format(data[i]["posting_date"], 'Date') }}</td>
<td style="text-align: right">{{ data[i]["age"] }}</td>
<td>
{% if not(filters.show_future_payments) %}
{{ data[i]["voucher_type"] }}
<br>
{% endif %}
{{ data[i]["voucher_no"] }}
</td>
{% if not (filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td>{{ data[i]["sales_person"] }}</td>
{% endif %}
{% if not (filters.show_future_payments) and filters.show_remarks %}
<td>
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
{{ data[i]["party"] }}
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
<br> {{ data[i]["customer_name"] }}
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
<br> {{ data[i]["supplier_name"] }}
{% endif %}
{% endif %}
<div>
{% if data[i]["remarks"] %}
{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
{% endif %}
</div>
</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% else %}
<td></td>
{% if not(filters.show_future_payments) %}
<td></td>
{% endif %}
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td></td>
{% endif %}
<td></td>
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% endif %}
{% else %}
{% if(data[i]["party"] or "&nbsp;") %}
{% if not(data[i]["is_total_row"]) %}
<td>
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
{% if(not(filters.customer | filters.supplier)) %}
{{ data[i]["party"] }}
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
<br> {{ data[i]["customer_name"] }}
@@ -235,132 +311,73 @@
<br> {{ data[i]["supplier_name"] }}
{% endif %}
{% endif %}
<div>
{% if data[i]["remarks"] %}
{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
{% endif %}
</div>
<br>{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% else %}
<td></td>
{% if not(filters.show_future_payments) %}
<td></td>
{% endif %}
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td></td>
{% endif %}
<td></td>
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% endif %}
{% else %}
{% if(data[i]["party"] or "&nbsp;") %}
{% if not(data[i]["is_total_row"]) %}
<td>
{% if(not(filters.customer | filters.supplier)) %}
{{ data[i]["party"] }}
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
<br> {{ data[i]["customer_name"] }}
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
<br> {{ data[i]["supplier_name"] }}
{% endif %}
{% endif %}
<br>{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
</td>
{% else %}
<td><b>{{ _("Total") }}</b></td>
{% endif %}
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
<td><b>{{ _("Total") }}</b></td>
{% endif %}
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% endif %}
</tr>
{% endfor %}
<td></td>
<td></td>
{% endif %}
</tr>
{% endfor %}
<td></td>
<td></td>
{% if (filters.show_future_payments) or filters.show_remarks %}
<td></td>
{% endif %}
{% if not(filters.show_future_payments) %}
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
</tbody>
</table>
<br>
{% if ageing %}
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
{{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}}
</h4>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">0 - 30 Days</th>
<th style="width: 25%">30 - 60 Days</th>
<th style="width: 25%">60 - 90 Days</th>
<th style="width: 25%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>
{% endif %}
{% if terms_and_conditions %}
<div>
{{ terms_and_conditions }}
</div>
{% endif %}
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
{% else %}
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
{% endif %}
</tbody>
</table>
<br>
{% if ageing %}
<h4 class="text-center">
{{ _("Ageing Report based on {0} up to {1}").format(
ageing.ageing_based_on,
frappe.format(filters.report_date, "Date")
) }}
</h4>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">{{ _("0 - 30 Days") }}</th>
<th style="width: 25%">{{ _("30 - 60 Days") }}</th>
<th style="width: 25%">{{ _("60 - 90 Days") }}</th>
<th style="width: 25%">{{ _("90 - 120 Days") }}</th>
<th style="width: 20%">{{ _("Above 120 Days") }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>
{% endif %}
{% if terms_and_conditions %}
<div>
{{ terms_and_conditions }}
</div>
{% endif %}
<p class="text-right text-muted">{{ _("Printed on {0}").format(frappe.utils.now()) }}</p>

View File

@@ -215,16 +215,22 @@ def start_repost(account_repost_doc=str) -> None:
def get_allowed_types_from_settings(child_doc: bool = False):
repost_docs = [
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
)
]
# Avoid DISTINCT(...) here: Frappe applies a default ORDER BY which breaks on Postgres
# when used with SELECT DISTINCT.
repost_docs = frappe.db.get_all(
"Repost Allowed Types",
filters={"allowed": True},
pluck="document_type",
)
# De-dupe while preserving order (first occurrence wins)
repost_docs = list(dict.fromkeys(repost_docs))
result = repost_docs
if repost_docs and child_doc:
result.extend(get_child_docs(repost_docs))
# Keep uniqueness after extending
result = list(dict.fromkeys(result))
return result
@@ -291,8 +297,11 @@ def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})
if allowed_types := frappe.db.get_all(
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
):
return allowed_types
return []
allowed_types = frappe.db.get_all(
"Repost Allowed Types",
filters=filters,
pluck="document_type",
)
allowed_types = list(dict.fromkeys(allowed_types))
return [[dt] for dt in allowed_types]

View File

@@ -2974,6 +2974,60 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
discount_account = create_account(

View File

@@ -12,6 +12,7 @@ from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
@@ -612,6 +613,18 @@ def update_accounting_dimensions(round_off_gle):
for dimension in dimensions:
round_off_gle[dimension] = dimension_values.get(dimension)
else:
report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (
round_off_gle.company == dimension.company
and (
(report_type == "Profit and Loss" and dimension.mandatory_for_pl)
or (report_type == "Balance Sheet" and dimension.mandatory_for_bs)
)
and dimension.default_dimension
):
round_off_gle[dimension.fieldname] = dimension.default_dimension
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):

View File

@@ -219,13 +219,18 @@ def get_net_profit(
has_value = False
gross_income_roots = [row for row in (gross_income or []) if not flt(row.get("indent"))]
non_gross_income_roots = [row for row in (non_gross_income or []) if not flt(row.get("indent"))]
gross_expense_roots = [row for row in (gross_expense or []) if not flt(row.get("indent"))]
non_gross_expense_roots = [row for row in (non_gross_expense or []) if not flt(row.get("indent"))]
for period in period_list:
key = period if consolidated else period.key
gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
gross_income_for_period = sum(flt(row.get(key, 0)) for row in gross_income_roots)
non_gross_income_for_period = sum(flt(row.get(key, 0)) for row in non_gross_income_roots)
gross_expense_for_period = sum(flt(row.get(key, 0)) for row in gross_expense_roots)
non_gross_expense_for_period = sum(flt(row.get(key, 0)) for row in non_gross_expense_roots)
total_income = gross_income_for_period + non_gross_income_for_period
total_expense = gross_expense_for_period + non_gross_expense_for_period

View File

@@ -105,7 +105,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
{
"total_tax": total_tax,
"total_other_charges": total_other_charges,
"total": d.base_net_amount + total_tax,
"total": d.base_net_amount + total_tax + total_other_charges,
"currency": company_currency,
}
)

View File

@@ -511,6 +511,7 @@ def reconcile_against_document(
doc.make_advance_gl_entries(entry=row)
else:
_delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map()
# Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference

View File

@@ -235,26 +235,64 @@ frappe.ui.form.on("Asset", {
},
toggle_reference_doc: function (frm) {
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
frm.set_df_property("purchase_invoice", "read_only", 1);
frm.set_df_property("purchase_receipt", "read_only", 1);
} else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) {
frm.toggle_reqd("purchase_receipt", 0);
frm.toggle_reqd("purchase_invoice", 0);
} else if (frm.doc.purchase_receipt) {
// if purchase receipt link is set then set PI disabled
frm.toggle_reqd("purchase_invoice", 0);
frm.set_df_property("purchase_invoice", "read_only", 1);
} else if (frm.doc.purchase_invoice) {
// if purchase invoice link is set then set PR disabled
frm.toggle_reqd("purchase_receipt", 0);
frm.set_df_property("purchase_receipt", "read_only", 1);
} else {
frm.toggle_reqd("purchase_receipt", 1);
frm.set_df_property("purchase_receipt", "read_only", 0);
frm.toggle_reqd("purchase_invoice", 1);
frm.set_df_property("purchase_invoice", "read_only", 0);
const is_submitted = frm.doc.docstatus === 1;
const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset;
const clear_field = (field) => {
if (frm.doc[field]) {
frm.set_value(field, "");
}
};
["purchase_receipt", "purchase_receipt_item", "purchase_invoice", "purchase_invoice_item"].forEach(
(field) => {
frm.toggle_reqd(field, 0);
frm.set_df_property(field, "read_only", 0);
}
);
if (is_submitted) {
[
"purchase_receipt",
"purchase_receipt_item",
"purchase_invoice",
"purchase_invoice_item",
].forEach((field) => {
frm.set_df_property(field, "read_only", 1);
});
return;
}
if (is_special_asset) {
clear_field("purchase_receipt");
clear_field("purchase_receipt_item");
clear_field("purchase_invoice");
clear_field("purchase_invoice_item");
return;
}
if (frm.doc.purchase_receipt) {
frm.toggle_reqd("purchase_receipt_item", 1);
["purchase_invoice", "purchase_invoice_item"].forEach((field) => {
clear_field(field);
frm.set_df_property(field, "read_only", 1);
});
return;
}
if (frm.doc.purchase_invoice) {
frm.toggle_reqd("purchase_invoice_item", 1);
["purchase_receipt", "purchase_receipt_item"].forEach((field) => {
clear_field(field);
frm.set_df_property(field, "read_only", 1);
});
return;
}
frm.toggle_reqd("purchase_receipt", 1);
frm.toggle_reqd("purchase_invoice", 1);
},
make_journal_entry: function (frm) {
@@ -484,7 +522,6 @@ frappe.ui.form.on("Asset", {
} else {
frm.set_df_property("gross_purchase_amount", "read_only", 0);
}
frm.trigger("toggle_reference_doc");
},

View File

@@ -177,6 +177,7 @@
"default": "1",
"fieldname": "target_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Target Qty"
},
{
@@ -290,10 +291,10 @@
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
@@ -324,7 +325,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-08 13:14:33.008458",
"modified": "2026-01-13 17:25:01.352568",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",
@@ -362,10 +363,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -76,6 +76,7 @@ class AssetCapitalization(StockController):
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
posting_date: DF.Date
posting_time: DF.Time
project: DF.Link | None
service_items: DF.Table[AssetCapitalizationServiceItem]
service_items_total: DF.Currency
set_posting_time: DF.Check

View File

@@ -57,7 +57,7 @@ class AssetValueAdjustment(Document):
)
def on_cancel(self):
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
self.cancel_asset_revaluation_entry()
self.update_asset()
add_asset_activity(
self.asset,
@@ -159,6 +159,16 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
def cancel_asset_revaluation_entry(self):
if not self.journal_entry:
return
revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry)
if revaluation_entry.docstatus == 1:
revaluation_entry.flags.ignore_permissions = True
revaluation_entry.flags.via_asset_value_adjustment = True
revaluation_entry.cancel()
def update_asset(self, asset_value=None):
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
asset = self.update_asset_value_after_depreciation(difference_amount)

View File

@@ -794,7 +794,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
@frappe.whitelist()
def make_purchase_invoice_from_portal(purchase_order_name):
doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True)
if doc.contact_email != frappe.session.user:
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
doc.save()
frappe.db.commit()

View File

@@ -1297,6 +1297,55 @@ class TestPurchaseOrder(FrappeTestCase):
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 50)
def test_multiple_advances_against_purchase_order_are_allocated_across_partial_purchase_invoices(self):
# step - 1: create PO
po = create_purchase_order(qty=10, rate=10)
# step - 2: create first partial advance payment
pe1 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe1.reference_no = "1"
pe1.reference_date = nowdate()
pe1.paid_amount = 50
pe1.references[0].allocated_amount = 50
pe1.save(ignore_permissions=True).submit()
# check first advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 50)
# step - 3: create first PI for partial qty and allocate first advance
pi_1 = make_pi_from_po(po.name)
pi_1.update_stock = 1
pi_1.allocate_advances_automatically = 1
pi_1.items[0].qty = 5
pi_1.save(ignore_permissions=True).submit()
# step - 4: create second advance payment for remaining
pe2 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe2.reference_no = "2"
pe2.reference_date = nowdate()
pe2.paid_amount = 50
pe2.references[0].allocated_amount = 50
pe2.save(ignore_permissions=True).submit()
# check second advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 100)
# step - 5: create second PI for remaining qty and allocate second advance
pi_2 = make_pi_from_po(po.name)
pi_2.update_stock = 1
pi_2.allocate_advances_automatically = 1
pi_2.save(ignore_permissions=True).submit()
# check PO and PI status
po.reload()
pi_1.reload()
pi_2.reload()
self.assertEqual(pi_1.status, "Paid")
self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed")
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -34,15 +34,6 @@ frappe.ui.form.on("Request for Quotation", {
});
},
onload: function (frm) {
if (!frm.doc.message_for_supplier) {
frm.set_value(
"message_for_supplier",
__("Please supply the specified items at the best possible rates")
);
}
},
refresh: function (frm, cdt, cdn) {
if (frm.doc.docstatus === 1) {
frm.add_custom_button(
@@ -248,6 +239,25 @@ frappe.ui.form.on("Request for Quotation", {
}
refresh_field("items");
},
email_template(frm) {
if (frm.doc.email_template) {
frappe.db
.get_value("Email Template", frm.doc.email_template, [
"use_html",
"response",
"response_html",
"subject",
])
.then((r) => {
frm.set_value(
"message_for_supplier",
r.message.use_html ? r.message.response_html : r.message.response
);
frm.set_value("subject", r.message.subject);
});
}
},
preview: (frm) => {
let dialog = new frappe.ui.Dialog({
title: __("Preview Email"),

View File

@@ -30,6 +30,7 @@
"send_attached_files",
"send_document_print",
"sec_break_email_2",
"subject",
"message_for_supplier",
"terms_section_break",
"incoterm",
@@ -126,6 +127,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.suppliers.some((item) => item.send_email) && !(doc.docstatus == 1 && !doc.email_template)",
"fieldname": "supplier_response_section",
"fieldtype": "Section Break",
"label": "Email Details"
@@ -139,8 +141,7 @@
},
{
"allow_on_submit": 1,
"fetch_from": "email_template.response",
"fetch_if_empty": 1,
"default": "Please supply the specified items at the best possible rates",
"fieldname": "message_for_supplier",
"fieldtype": "Text Editor",
"in_list_view": 1,
@@ -251,7 +252,7 @@
"label": "Preview Email"
},
{
"depends_on": "eval:!doc.__islocal",
"depends_on": "eval:doc.suppliers.some((item) => item.send_email)",
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
@@ -315,6 +316,14 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"default": "Request for Quotation",
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"not_nullable": 1,
"reqd": 1
}
],
"grid_page_length": 50,
@@ -322,7 +331,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-03-03 16:48:39.856779",
"modified": "2026-01-05 14:27:33.329810",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@@ -393,4 +402,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -56,6 +56,7 @@ class RequestforQuotation(BuyingController):
send_attached_files: DF.Check
send_document_print: DF.Check
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
subject: DF.Data
suppliers: DF.Table[RequestforQuotationSupplier]
tc_name: DF.Link | None
terms: DF.TextEditor | None
@@ -66,6 +67,7 @@ class RequestforQuotation(BuyingController):
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
self.set_data_for_supplier()
def validate(self):
self.validate_duplicate_supplier()
@@ -90,6 +92,19 @@ class RequestforQuotation(BuyingController):
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
)
def set_data_for_supplier(self):
if self.email_template:
data = frappe.get_value(
"Email Template",
self.email_template,
["use_html", "response", "response_html", "subject"],
as_dict=True,
)
if not self.message_for_supplier:
self.message_for_supplier = data.response_html if data.use_html else data.response
if not self.subject:
self.subject = data.subject
def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers]
if len(supplier_list) != len(set(supplier_list)):
@@ -283,12 +298,6 @@ class RequestforQuotation(BuyingController):
}
)
if not self.email_template:
return
email_template = frappe.get_doc("Email Template", self.email_template)
message = frappe.render_template(email_template.response_, doc_args)
subject = frappe.render_template(email_template.subject, doc_args)
fixed_procurement_email = frappe.db.get_single_value("Buying Settings", "fixed_email")
if fixed_procurement_email:
sender = frappe.db.get_value("Email Account", fixed_procurement_email, "email_id")
@@ -296,7 +305,12 @@ class RequestforQuotation(BuyingController):
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
if preview:
return {"message": message, "subject": subject}
return {
"message": self.message_for_supplier,
"subject": self.subject
or frappe.get_value("Email Template", self.email_template, "subject")
or _("Request for Quotation"),
}
attachments = []
if self.send_attached_files:
@@ -316,7 +330,15 @@ class RequestforQuotation(BuyingController):
)
)
self.send_email(data, sender, subject, message, attachments)
self.send_email(
data,
sender,
self.subject
or frappe.get_value("Email Template", self.email_template, "subject")
or _("Request for Quotation"),
self.message_for_supplier,
attachments,
)
def send_email(self, data, sender, subject, message, attachments):
make(

View File

@@ -80,20 +80,21 @@
"fieldname": "email_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email Id",
"label": "Email ID",
"no_copy": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-04 22:01:43.832942",
"modified": "2026-01-05 14:08:27.274538",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation Supplier",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -0,0 +1,9 @@
# Version 16 Released!
ERPNext version 16 has been released!
Since it's the latest version of ERPNext, we recommend that you update to it to get the latest features, bug fixes and other improvements.
[Click here to know more about v16](https://frappe.io/erpnext/version-16)
If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to learn how to update to v16](https://docs.frappe.io/cloud/sites/version-upgrade)

View File

@@ -184,9 +184,8 @@ class AccountsController(TransactionBase):
msg = ""
if self.get("update_outstanding_for_self"):
msg = (
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
"uncheck '{2}' checkbox. <br><br>Or"
msg = _(
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox."
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
@@ -197,8 +196,8 @@ class AccountsController(TransactionBase):
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
):
self.update_outstanding_for_self = 1
msg = (
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
msg = _(
"The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice."
).format(
against_voucher_outstanding,
get_link_to_form(self.doctype, self.get("return_against")),
@@ -206,11 +205,11 @@ class AccountsController(TransactionBase):
)
if msg:
msg += " you can use {} tool to reconcile against {} later.".format(
msg += "<br><br>" + _("You can use {0} to reconcile against {1} later.").format(
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
)
frappe.msgprint(_(msg))
frappe.msgprint(msg)
def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"):
@@ -3714,9 +3713,9 @@ def validate_child_on_delete(row, parent):
)
if flt(row.ordered_qty):
frappe.throw(
_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(
row.idx, row.item_code
)
_(
"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
).format(row.idx, row.item_code)
)
if parent.doctype == "Purchase Order" and flt(row.received_qty):

View File

@@ -1004,10 +1004,19 @@ class SellingController(StockController):
def set_default_income_account_for_item(obj):
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
"""Set income account as default for items in the transaction.
Updates the item default income account for each item in the transaction
if it differs from the company's default income account.
Args:
obj: Transaction document containing items table with income_account field
"""
company_default = frappe.get_cached_value("Company", obj.company, "default_income_account")
for d in obj.get("items", default=[]):
income_account = getattr(d, "income_account", None)
if d.item_code and income_account and income_account != company_default:
set_item_default(d.item_code, obj.company, "income_account", income_account)
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):

View File

@@ -46,17 +46,23 @@ class calculate_taxes_and_totals:
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
return items
def calculate(self):
def calculate(self, ignore_tax_template_validation=False):
if not len(self.doc.items):
return
self.discount_amount_applied = False
self.need_recomputation = False
self.ignore_tax_template_validation = ignore_tax_template_validation
self._calculate()
if self.doc.meta.get_field("discount_amount"):
self.set_discount_amount()
self.apply_discount_amount()
if not ignore_tax_template_validation and self.need_recomputation:
return self.calculate(ignore_tax_template_validation=True)
# Update grand total as per cash and non trade discount
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount
@@ -100,6 +106,9 @@ class calculate_taxes_and_totals:
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
if self.ignore_tax_template_validation:
return
if self.doc.get("is_return") and self.doc.get("return_against"):
return
@@ -141,6 +150,10 @@ class calculate_taxes_and_totals:
)
)
# For correct tax_amount calculation re-computation is required
if self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total":
self.need_recomputation = True
def update_item_tax_map(self):
for item in self.doc.items:
item.item_tax_rate = get_item_tax_map(

View File

@@ -778,9 +778,8 @@ class TestSubcontractingController(FrappeTestCase):
row.serial_no = "ABC"
break
bundle.save()
self.assertRaises(frappe.ValidationError, bundle.save)
self.assertRaises(frappe.ValidationError, scr1.save)
bundle.load_from_db()
for row in bundle.entries:
if row.idx == 1:

View File

@@ -559,6 +559,7 @@ accounting_dimension_doctypes = [
"Payment Request",
"Asset Movement Item",
"Asset Depreciation Schedule",
"Advance Taxes and Charges",
]
get_matching_queries = (

View File

@@ -315,8 +315,8 @@ class WorkOrder(Document):
def validate_work_order_against_so(self):
# already ordered qty
ordered_qty_against_so = frappe.db.sql(
"""select sum(qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""",
"""select sum(qty - process_loss_qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""",
(self.production_item, self.sales_order, self.name),
)[0][0]
@@ -351,15 +351,16 @@ class WorkOrder(Document):
def update_status(self, status=None):
"""Update status of work order if unknown"""
if status != "Stopped" and status != "Closed":
status = self.get_status(status)
if self.status != "Closed":
if status not in ["Stopped", "Closed"]:
status = self.get_status(status)
if status != self.status:
self.db_set("status", status)
if status != self.status:
self.db_set("status", status)
self.update_required_items()
self.update_required_items()
return status
return status or self.status
def get_status(self, status=None):
"""Return the status based on stock entries against this work order"""
@@ -515,6 +516,9 @@ class WorkOrder(Document):
self.validate_cancel()
self.db_set("status", "Cancelled")
self.on_close_or_cancel()
def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan}
):
@@ -842,7 +846,7 @@ class WorkOrder(Document):
qty = frappe.db.sql(
f""" select sum(qty) from
`tabWork Order` where sales_order = %s and docstatus = 1 and {cond}
`tabWork Order` where sales_order = %s and docstatus = 1 and status <> 'Closed' and {cond}
""",
(self.sales_order, (self.product_bundle_item or self.production_item)),
as_list=1,
@@ -1603,8 +1607,8 @@ def close_work_order(work_order, status):
)
)
work_order.on_close_or_cancel()
work_order.update_status(status)
work_order.update_planned_qty()
frappe.msgprint(_("Work Order has been {0}").format(status))
work_order.notify_update()
return work_order.status

View File

@@ -427,3 +427,4 @@ erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges

View File

@@ -0,0 +1,7 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Advance Taxes and Charges")

View File

@@ -8,12 +8,24 @@ def execute():
def update_delivery_note():
DN = frappe.qb.DocType("Delivery Note")
DNI = frappe.qb.DocType("Delivery Note Item")
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
IfNull(DN.pick_list, "") != ""
).run()
# Postgres doesn't support UPDATE ... JOIN. Use UPDATE ... FROM instead.
frappe.db.multisql(
{
"mariadb": """
UPDATE `tabDelivery Note Item` dni
JOIN `tabDelivery Note` dn ON dn.`name` = dni.`parent`
SET dni.`against_pick_list` = dn.`pick_list`
WHERE COALESCE(dn.`pick_list`, '') <> ''
""",
"postgres": """
UPDATE "tabDelivery Note Item" dni
SET against_pick_list = dn.pick_list
FROM "tabDelivery Note" dn
WHERE dn.name = dni.parent
AND COALESCE(dn.pick_list, '') <> ''
""",
}
)
def update_pick_list_items():

View File

@@ -479,7 +479,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {
if (row.barcode && !frappe.flags.trigger_from_barcode_scanner) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
frappe.model.set_value(cdt, cdn, {
"item_code": r.message.item_code,
@@ -555,10 +555,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var item = frappe.get_doc(cdt, cdn);
var update_stock = 0, show_batch_dialog = 0;
item.weight_per_unit = 0;
item.weight_uom = '';
item.uom = null // make UOM blank to update the existing UOM when item changes
if(!item.barcode){
item.uom = null // make UOM blank to update the existing UOM when item changes
}
item.conversion_factor = 0;
if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
@@ -574,6 +575,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
show_batch_dialog = 0;
}
item.barcode = null;
@@ -1330,9 +1332,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
plc_conversion_rate() {
if(this.frm.doc.price_list_currency === this.get_company_currency()) {
this.frm.set_value("plc_conversion_rate", 1.0);
} else if(this.frm.doc.price_list_currency === this.frm.doc.currency
&& this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 &&
cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)) {
} else if (
this.frm.doc.price_list_currency === this.frm.doc.currency &&
this.frm.doc.plc_conversion_rate &&
flt(this.frm.doc.plc_conversion_rate) != 1 &&
flt(this.frm.doc.plc_conversion_rate) != flt(this.frm.doc.conversion_rate)
) {
this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
}

View File

@@ -138,7 +138,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
@@ -148,6 +147,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.clean_up(),
() => this.set_barcode_uom(row, uom),
() => this.revert_selector_flag(),
() => resolve(row),
]);
@@ -404,6 +404,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
} else {
row.barcode = barcode;
}
}

View File

@@ -1869,12 +1869,13 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty))
.select(Sum(wo.qty - wo.process_loss_qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name)
& (wo.sales_order_item == i.name)
& (wo.docstatus.lt(2))
& (wo.status != "Closed")
)
.run()[0][0]
)

View File

@@ -606,6 +606,9 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) {
frappe.dom.freeze();
if (this.frm.doc.set_warehouse !== this.settings.warehouse) {
this.frm.set_value("set_warehouse", this.settings.warehouse);
}
let item_row = undefined;
try {
let { field, value, item } = args;

View File

@@ -73,6 +73,18 @@ frappe.query_reports["Sales Analytics"] = {
default: "Monthly",
reqd: 1,
},
{
fieldname: "curves",
label: __("Curves"),
fieldtype: "Select",
options: [
{ value: "all", label: __("All") },
{ value: "non-zeros", label: __("Non-Zeros") },
{ value: "total", label: __("Total Only") },
],
default: "all",
reqd: 1,
},
{
fieldname: "show_aggregate_value_from_subsidiary_companies",
label: __("Show Aggregate Value from Subsidiary Companies"),

View File

@@ -460,7 +460,31 @@ class Analytics:
labels = [d.get("label") for d in self.columns[3 : length - 1]]
else:
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
datasets = []
for curve in self.data:
data = {
"name": curve.get("entity_name", curve["entity"]),
"values": [curve.get(scrub(label), 0) for label in labels],
}
if self.filters.curves == "non-zeros" and not sum(data["values"]):
continue
elif self.filters.curves == "total" and "indent" in curve:
if curve["indent"] == 0:
datasets.append(data)
elif self.filters.curves == "total":
if datasets:
a = [
data["values"][idx] + datasets[0]["values"][idx] for idx in range(len(data["values"]))
]
datasets[0]["values"] = a
else:
datasets.append(data)
datasets[0]["name"] = _("Total")
else:
datasets.append(data)
self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"}
if self.filters["value_quantity"] == "Value":
self.chart["fieldtype"] = "Currency"

View File

@@ -11,7 +11,16 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_to_form, get_timestamp, today
from frappe.utils import (
add_months,
cint,
formatdate,
get_first_day,
get_last_day,
get_link_to_form,
get_timestamp,
today,
)
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
@@ -762,31 +771,41 @@ def install_country_fixtures(company, country):
def update_company_current_month_sales(company):
from_date = get_first_day(today())
to_date = get_first_day(add_months(from_date, 1))
"""Update Company's Total Monthly Sales.
results = frappe.db.sql(
"""
SELECT
SUM(base_grand_total) AS total,
DATE_FORMAT(posting_date, '%%m-%%Y') AS month_year
FROM
`tabSales Invoice`
WHERE
posting_date >= %s
AND posting_date < %s
AND docstatus = 1
AND company = %s
GROUP BY
month_year
""",
(from_date, to_date, company),
as_dict=True,
Postgres compatibility:
- Avoid MariaDB-only DATE_FORMAT().
- Use a date range for the current month instead (portable + index-friendly).
"""
# Local imports so you don't have to touch file-level imports
from frappe.query_builder.functions import Sum
start_date = get_first_day(today())
end_date = get_last_day(today())
si = frappe.qb.DocType("Sales Invoice")
total_monthly_sales = (
frappe.qb.from_(si)
.select(Sum(si.base_grand_total))
.where(
(si.docstatus == 1)
& (si.company == company)
& (si.posting_date >= start_date)
& (si.posting_date <= end_date)
)
).run(pluck=True)[0] or 0
# Fieldname in standard ERPNext is `total_monthly_sales`
frappe.db.set_value(
"Company",
company,
"total_monthly_sales",
total_monthly_sales,
update_modified=False,
)
monthly_total = results[0]["total"] if len(results) > 0 else 0
frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total)
def update_company_monthly_sales(company):
"""Cache past year monthly sales of every company based on sales invoices"""

View File

@@ -2790,6 +2790,23 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty)
self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty)
def test_negative_stock_with_higher_precision(self):
original_flt_precision = frappe.db.get_default("float_precision")
frappe.db.set_single_value("System Settings", "float_precision", 7)
item_code = make_item(
"Test Negative Stock High Precision Item", properties={"is_stock_item": 1, "valuation_rate": 1}
).name
dn = create_delivery_note(
item_code=item_code,
qty=0.0000010,
do_not_submit=True,
)
self.assertRaises(frappe.ValidationError, dn.submit)
frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -977,7 +977,7 @@ frappe.tour["Item"] = [
fieldname: "valuation_rate",
title: "Valuation Rate",
description: __(
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
),
},
{

View File

@@ -273,6 +273,9 @@ class MaterialRequest(BuyingController):
.groupby(doctype.material_request_item)
)
if self.material_request_type == "Manufacture":
query = query.where(doctype.status != "Closed")
mr_items_ordered_qty = frappe._dict(query.run())
return mr_items_ordered_qty

View File

@@ -1004,7 +1004,8 @@ def make_material_request(**args):
mr = frappe.new_doc("Material Request")
mr.material_request_type = args.material_request_type or "Purchase"
mr.company = args.company or "_Test Company"
mr.customer = args.customer or "_Test Customer"
if mr.material_request_type == "Customer Provided":
mr.customer = args.customer or "_Test Customer"
mr.append(
"items",
{
@@ -1013,6 +1014,7 @@ def make_material_request(**args):
"uom": args.uom or "_Test UOM",
"conversion_factor": args.conversion_factor or 1,
"schedule_date": args.schedule_date or today(),
"from_warehouse": args.from_warehouse,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
},

View File

@@ -383,7 +383,7 @@ class PickList(TransactionBase):
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
self.validate_picked_qty(picked_items)
doc_updates = {}
doc_updates = {item: {"picked_qty": 0} for item in set(packed_items)}
for d in picked_items:
doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)}
@@ -394,7 +394,7 @@ class PickList(TransactionBase):
picked_items = get_picked_items_qty(so_items)
self.validate_picked_qty(picked_items)
doc_updates = {}
doc_updates = {item: {"picked_qty": 0} for item in set(so_items)}
for d in picked_items:
doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)}

View File

@@ -4529,6 +4529,154 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(sles, [1500.0, 1500.0])
def test_do_not_use_batchwise_valuation_with_fifo(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = make_item(
"Test Item Do Not Use Batchwise Valuation with FIFO",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BN-TESTDNUBVWF-.#####",
"valuation_method": "FIFO",
},
).name
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00001",
"item": item_code,
}
).insert()
doc.db_set("use_batchwise_valuation", 0)
doc.reload()
self.assertTrue(doc.use_batchwise_valuation == 0)
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00002",
"item": item_code,
}
).insert()
self.assertTrue(doc.use_batchwise_valuation == 1)
warehouse = "_Test Warehouse - _TC"
make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
se1 = make_stock_entry(
item_code=item_code,
qty=10,
rate=200,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se1.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se2 = make_stock_entry(
item_code=item_code,
qty=10,
rate=2,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00002",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se2.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se3 = make_stock_entry(
item_code=item_code,
qty=20,
source=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
ste_details = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se3.name,
},
["stock_queue", "stock_value_difference"],
as_dict=1,
)
stock_queue = frappe.parse_json(ste_details.stock_queue)
self.assertEqual(stock_queue, [])
self.assertEqual(ste_details.stock_value_difference, 3000 * -1)
se4 = make_stock_entry(
item_code=item_code,
qty=20,
rate=0,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
do_not_submit=1,
)
se4.items[0].basic_rate = 0.0
se4.items[0].allow_zero_valuation_rate = 1
se4.submit()
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se4.name,
},
"stock_queue",
)
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -116,10 +116,20 @@ class SerialandBatchBundle(Document):
return
self.allow_existing_serial_nos()
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
self.validate_serial_nos_duplicate()
if self.docstatus == 1:
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
self.validate_serial_nos_duplicate()
self.check_future_entries_exists()
elif (
self.has_serial_no
and self.type_of_transaction == "Outward"
and self.voucher_type != "Stock Reconciliation"
and self.voucher_no
):
self.validate_serial_no_status()
self.check_future_entries_exists()
self.set_is_outward()
self.calculate_total_qty()
self.set_warehouse()
@@ -129,6 +139,25 @@ class SerialandBatchBundle(Document):
self.calculate_qty_and_amount()
def validate_serial_no_status(self):
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
invalid_serial_nos = frappe.get_all(
"Serial No",
filters={
"name": ("in", serial_nos),
"warehouse": ("!=", self.warehouse),
},
pluck="name",
)
if invalid_serial_nos:
msg = _(
"You cannot outward following {0} as either they are Delivered, Inactive or located in a different warehouse."
).format(_("Serial Nos") if len(invalid_serial_nos) > 1 else _("Serial No"))
msg += "<hr>"
msg += ", ".join(sn for sn in invalid_serial_nos)
frappe.throw(msg)
def validate_voucher_detail_no(self):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
@@ -684,17 +713,16 @@ class SerialandBatchBundle(Document):
is_packed_item = True
stock_queue = []
batches = []
if prev_sle and prev_sle.stock_queue:
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if prev_sle and prev_sle.stock_queue and parse_json(prev_sle.stock_queue):
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
@@ -702,10 +730,16 @@ class SerialandBatchBundle(Document):
"Buying Settings", "set_valuation_rate_for_rejected_materials"
)
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
for d in self.entries:
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:
elif (
(flt(d.incoming_rate, precision) == flt(rate, precision))
and not stock_queue
and d.qty
and d.stock_value_difference
):
continue
if is_packed_item and d.incoming_rate:
@@ -715,7 +749,7 @@ class SerialandBatchBundle(Document):
if d.qty:
d.stock_value_difference = flt(d.qty) * d.incoming_rate
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
if valuation_method == "FIFO" and d.batch_no in batches and d.incoming_rate is not None:
stock_queue.append([d.qty, d.incoming_rate])
d.stock_queue = json.dumps(stock_queue)
@@ -766,7 +800,7 @@ class SerialandBatchBundle(Document):
self.calculate_total_qty(save=True)
# If user has changed the rate in the child table
if self.docstatus == 0:
if self.docstatus == 0 and self.type_of_transaction == "Inward":
self.set_incoming_rate(parent=parent, row=row, save=True)
if self.docstatus == 0 and parent.get("is_return") and parent.is_new():

View File

@@ -982,6 +982,7 @@ def make_serial_batch_bundle(kwargs):
"type_of_transaction": type_of_transaction,
"company": kwargs.company or "_Test Company",
"do_not_submit": kwargs.do_not_submit,
"ignore_sabb_validation": kwargs.ignore_sabb_validation or False,
}
)

View File

@@ -382,6 +382,7 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "tracking_status",
"fieldtype": "Select",
"label": "Tracking Status",
@@ -440,7 +441,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2025-02-20 16:55:20.076418",
"modified": "2026-01-07 19:24:23.566312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Shipment",

View File

@@ -20,9 +20,7 @@ class Shipment(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import (
ShipmentDeliveryNote,
)
from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import ShipmentDeliveryNote
from erpnext.stock.doctype.shipment_parcel.shipment_parcel import ShipmentParcel
amended_from: DF.Link | None

View File

@@ -646,7 +646,7 @@ class StockEntry(StockController):
"Material Transfer for Manufacture",
]
validate_for_manufacture = any([d.bom_no for d in self.get("items")])
has_bom = any([d.bom_no for d in self.get("items")])
if self.purpose in source_mandatory and self.purpose not in target_mandatory:
self.to_warehouse = None
@@ -675,7 +675,7 @@ class StockEntry(StockController):
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture":
if validate_for_manufacture:
if has_bom:
if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
@@ -685,6 +685,17 @@ class StockEntry(StockController):
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Disassemble":
if has_bom:
if d.is_finished_item:
d.t_warehouse = None
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
else:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if cstr(d.s_warehouse) == cstr(d.t_warehouse) and self.purpose not in [
"Material Transfer for Manufacture",
"Material Transfer",
@@ -1907,9 +1918,12 @@ class StockEntry(StockController):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
if self.work_order:
return self._add_items_for_disassembly_from_work_order()
return self._add_items_for_disassembly_from_bom()
def _add_items_for_disassembly_from_work_order(self):
items = self.get_items_from_manufacture_entry()
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
@@ -1941,6 +1955,23 @@ class StockEntry(StockController):
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
def _add_items_for_disassembly_from_bom(self):
if not self.bom_no or not self.fg_completed_qty:
frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly"))
# Raw Materials
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
for item_row in item_dict.values():
item_row["to_warehouse"] = self.to_warehouse
item_row["from_warehouse"] = ""
item_row["is_finished_item"] = 0
self.add_to_stock_entry_detail(item_dict)
# Finished goods
self.load_items_from_bom()
def get_items_from_manufacture_entry(self):
return frappe.get_all(
"Stock Entry",
@@ -2164,6 +2195,7 @@ class StockEntry(StockController):
expense_account = item.get("expense_account")
if not expense_account:
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
@@ -2176,6 +2208,15 @@ class StockEntry(StockController):
"is_finished_item": 1,
}
if self.purpose == "Disassemble":
args.update(
{
"from_warehouse": self.from_warehouse,
"to_warehouse": "",
"qty": flt(self.fg_completed_qty),
}
)
if (
self.work_order
and self.pro_doc.has_batch_no
@@ -2807,7 +2848,7 @@ class StockEntry(StockController):
stock_entries_child_list.append(d.ste_detail)
transferred_qty = frappe.get_all(
"Stock Entry Detail",
fields=["sum(qty) as qty"],
fields=["sum(transfer_qty) as qty"],
filters={
"against_stock_entry": d.against_stock_entry,
"ste_detail": d.ste_detail,

View File

@@ -14,6 +14,13 @@ from erpnext.stock.doctype.item.test_item import (
make_item_variant,
set_item_variant_settings,
)
from erpnext.stock.doctype.material_request.material_request import (
make_in_transit_stock_entry,
)
from erpnext.stock.doctype.material_request.test_material_request import (
get_in_transit_warehouse,
make_material_request,
)
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
@@ -2127,6 +2134,104 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.purpose, "Repack")
self.assertRaises(frappe.ValidationError, se.submit)
def test_transferred_qty_in_material_transfer(self):
item_code = "_Test Item"
source_warehouse = "_Test Warehouse - _TC"
target_warehouse = "_Test Warehouse 1 - _TC"
if not frappe.db.get_value("UOM Conversion Detail", {"parent": item_code, "uom": "Box"}):
item_doc = frappe.get_doc("Item", item_code)
item_doc.append("uoms", {"uom": "Box", "conversion_factor": 12})
item_doc.save(ignore_permissions=True)
make_stock_entry(item_code=item_code, target=source_warehouse, qty=12, rate=100)
# Create a Material Request for Material Transfer
material_request = make_material_request(
material_request_type="Material Transfer",
qty=1,
item_code=item_code,
uom="Box",
conversion_factor=12,
from_warehouse=source_warehouse,
warehouse=target_warehouse,
)
in_transit_wh = get_in_transit_warehouse(material_request.company)
# Create first Stock Entry (Source -> In-Transit)
stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_1.items[0].update(
{
"qty": 1,
"s_warehouse": source_warehouse,
}
)
stock_entry_1.save().submit()
# Validate transfer status after first transfer
material_request.reload()
self.assertEqual(material_request.transfer_status, "In Transit")
# Create final Stock Entry (In-Transit -> Target)
end_transit_1 = make_stock_in_entry(stock_entry_1.name)
end_transit_1.save().submit()
end_transit_1.reload()
# Validate quantities
stock_entry_1.reload()
self.assertEqual(stock_entry_1.items[0].qty, 1)
self.assertEqual(stock_entry_1.items[0].transfer_qty, 12)
self.assertEqual(stock_entry_1.items[0].transferred_qty, 12)
# Validate transfer status after final transfer
material_request.reload()
self.assertEqual(material_request.transfer_status, "Completed")
def test_disassemble_entry_without_wo(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
fg_item = make_item("_Disassemble Mobile", properties={"is_stock_item": 1}).name
rm_item1 = make_item("_Disassemble Temper Glass", properties={"is_stock_item": 1}).name
rm_item2 = make_item("_Disassemble Battery", properties={"is_stock_item": 1}).name
warehouse = "_Test Warehouse - _TC"
# Stock up the FG item (what we'll disassemble)
make_stock_entry(item_code=fg_item, target=warehouse, qty=5, purpose="Material Receipt")
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
se = make_stock_entry(item_code=fg_item, qty=1, purpose="Disassemble", do_not_save=True)
se.from_bom = 1
se.use_multi_level_bom = 1
se.bom_no = bom_no
se.fg_completed_qty = 1
se.from_warehouse = warehouse
se.to_warehouse = warehouse
se.get_items()
# Verify FG as source (being consumed)
fg_items = [d for d in se.items if d.is_finished_item]
self.assertEqual(len(fg_items), 1)
self.assertEqual(fg_items[0].item_code, fg_item)
self.assertEqual(fg_items[0].qty, 1)
self.assertEqual(fg_items[0].s_warehouse, warehouse)
self.assertFalse(fg_items[0].t_warehouse)
# Verify RM as target (being received)
rm_items = {d.item_code: d for d in se.items if not d.is_finished_item}
self.assertEqual(len(rm_items), 2)
self.assertIn(rm_item1, rm_items)
self.assertIn(rm_item2, rm_items)
self.assertEqual(rm_items[rm_item1].qty, 1)
self.assertEqual(rm_items[rm_item2].qty, 1)
self.assertEqual(rm_items[rm_item1].t_warehouse, warehouse)
self.assertFalse(rm_items[rm_item1].s_warehouse)
se.calculate_rate_and_amount()
se.save()
se.submit()
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -74,6 +74,7 @@ class StockReconciliation(StockController):
self.validate_duplicate_serial_and_batch_bundle("items")
self.remove_items_with_no_change()
self.validate_data()
self.change_row_indexes()
self.validate_expense_account()
self.validate_customer_provided_item()
self.set_zero_value_for_customer_provided_items()
@@ -557,8 +558,7 @@ class StockReconciliation(StockController):
elif len(items) != len(self.items):
self.items = items
for i, item in enumerate(self.items):
item.idx = i + 1
self.change_idx = True
frappe.msgprint(_("Removed items with no change in quantity or value."))
def calculate_difference_amount(self, item, item_dict):
@@ -575,14 +575,14 @@ class StockReconciliation(StockController):
def validate_data(self):
def _get_msg(row_num, msg):
return _("Row # {0}:").format(row_num + 1) + " " + msg
return _("Row #{0}:").format(row_num) + " " + msg
self.validation_messages = []
item_warehouse_combinations = []
default_currency = frappe.db.get_default("currency")
for row_num, row in enumerate(self.items):
for row in self.items:
# find duplicates
key = [row.item_code, row.warehouse]
for field in ["serial_no", "batch_no"]:
@@ -595,7 +595,7 @@ class StockReconciliation(StockController):
if key in item_warehouse_combinations:
self.validation_messages.append(
_get_msg(row_num, _("Same item and warehouse combination already entered."))
_get_msg(row.idx, _("Same item and warehouse combination already entered."))
)
else:
item_warehouse_combinations.append(key)
@@ -605,29 +605,29 @@ class StockReconciliation(StockController):
if row.serial_no and not row.qty:
self.validation_messages.append(
_get_msg(
row_num,
row.idx,
f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified",
)
)
# validate warehouse
if not frappe.db.get_value("Warehouse", row.warehouse):
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
self.validation_messages.append(_get_msg(row.idx, _("Warehouse not found in the system")))
# if both not specified
if row.qty in ["", None] and row.valuation_rate in ["", None]:
self.validation_messages.append(
_get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both"))
_get_msg(row.idx, _("Please specify either Quantity or Valuation Rate or both"))
)
# do not allow negative quantity
if flt(row.qty) < 0:
self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed")))
self.validation_messages.append(_get_msg(row.idx, _("Negative Quantity is not allowed")))
# do not allow negative valuation
if flt(row.valuation_rate) < 0:
self.validation_messages.append(
_get_msg(row_num, _("Negative Valuation Rate is not allowed"))
_get_msg(row.idx, _("Negative Valuation Rate is not allowed"))
)
if row.qty and row.valuation_rate in ["", None]:
@@ -659,6 +659,11 @@ class StockReconciliation(StockController):
raise frappe.ValidationError(self.validation_messages)
def change_row_indexes(self):
if getattr(self, "change_idx", False):
for i, item in enumerate(self.items):
item.idx = i + 1
def validate_item(self, item_code, row):
from erpnext.stock.doctype.item.item import (
validate_cancelled_item,

View File

@@ -42,9 +42,37 @@ def get_data(report_filters):
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
d.difference_value = d.stock_value - d.account_value
d.ledger_type = "Stock Ledger Entry"
if abs(d.difference_value) > 0.1:
data.append(d)
if key in voucher_wise_gl_data:
del voucher_wise_gl_data[key]
if voucher_wise_gl_data:
data += get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data)
return data
def get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data):
data = []
for key in voucher_wise_gl_data:
gl_data = voucher_wise_gl_data.get(key) or {}
data.append(
{
"name": gl_data.get("name"),
"ledger_type": "GL Entry",
"voucher_type": gl_data.get("voucher_type"),
"voucher_no": gl_data.get("voucher_no"),
"posting_date": gl_data.get("posting_date"),
"stock_value": 0,
"account_value": gl_data.get("account_value", 0),
"difference_value": gl_data.get("account_value", 0) * -1,
}
)
return data
@@ -89,6 +117,7 @@ def get_gl_data(report_filters, filters):
"voucher_type",
"voucher_no",
"sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value",
"posting_date",
],
group_by="voucher_type, voucher_no",
)
@@ -106,10 +135,15 @@ def get_columns(filters):
{
"label": _("Stock Ledger ID"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Stock Ledger Entry",
"fieldtype": "Dynamic Link",
"options": "ledger_type",
"width": "80",
},
{
"label": _("Ledger Type"),
"fieldname": "ledger_type",
"fieldtype": "Data",
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"},
{"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"},

View File

@@ -4,7 +4,7 @@ import frappe
from frappe import _, bold
from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
from pypika import Order
from pypika.terms import ExistsCriterion
@@ -616,8 +616,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
self.old_serial_nos = []
serial_nos = self.get_serial_nos()
result = self.get_serial_no_wise_incoming_rate(serial_nos)
for serial_no in serial_nos:
incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
incoming_rate = result.get(serial_no)
if incoming_rate is None:
self.old_serial_nos.append(serial_no)
continue
@@ -627,44 +628,103 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
self.calculate_stock_value_from_deprecarated_ledgers()
def get_incoming_rate_from_bundle(self, serial_no) -> float:
def get_serial_no_wise_incoming_rate(self, serial_nos):
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
def get_latest_based_on_posting_datetime():
# Get latest inward record based on posting datetime for each serial no
latest_posting = (
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.on(bundle.name == bundle_child.parent)
.select(
bundle_child.serial_no,
Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"),
)
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle.type_of_transaction == "Inward")
& (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
& (bundle_child.serial_no.isin(serial_nos))
)
.groupby(bundle_child.serial_no)
)
# Important to exclude the current voucher to calculate correct the stock value difference
if self.sle.voucher_no:
latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no)
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
timestamp_condition = CombineDatetime(
bundle.posting_date, bundle.posting_time
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
latest_posting = latest_posting.where(timestamp_condition)
latest_posting = latest_posting.as_("latest_posting")
return latest_posting
def get_latest_based_on_creation(latest_posting):
# Get latest inward record based on creation for each serial no
latest_creation = (
frappe.qb.from_(bundle)
.join(bundle_child)
.on(bundle.name == bundle_child.parent)
.join(latest_posting)
.on(
(latest_posting.serial_no == bundle_child.serial_no)
& (
latest_posting.max_posting_dt
== CombineDatetime(bundle.posting_date, bundle.posting_time)
)
)
.select(
bundle_child.serial_no,
Max(bundle.creation).as_("max_creation"),
)
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle.type_of_transaction == "Inward")
& (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
)
.groupby(bundle_child.serial_no)
).as_("latest_creation")
return latest_creation
latest_posting = get_latest_based_on_posting_datetime()
latest_creation = get_latest_based_on_creation(latest_posting)
query = (
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.join(bundle_child)
.on(bundle.name == bundle_child.parent)
.select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle_child.serial_no == serial_no)
& (bundle.type_of_transaction == "Inward")
& (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
.join(latest_creation)
.on(
(latest_creation.serial_no == bundle_child.serial_no)
& (latest_creation.max_creation == bundle.creation)
)
.select(
bundle_child.serial_no,
bundle_child.incoming_rate,
)
.orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
.limit(1)
)
# Important to exclude the current voucher to calculate correct the stock value difference
if self.sle.voucher_no:
query = query.where(bundle.voucher_no != self.sle.voucher_no)
result = query.run(as_list=1)
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
timestamp_condition = CombineDatetime(
bundle.posting_date, bundle.posting_time
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
query = query.where(timestamp_condition)
incoming_rate = query.run()
return flt(incoming_rate[0][0]) if incoming_rate else None
return frappe._dict(result) if result else frappe._dict({})
def get_serial_nos(self):
if self.sle.get("serial_nos"):
@@ -1131,6 +1191,9 @@ class SerialBatchCreation:
doc.submit()
else:
if self.get("ignore_sabb_validation"):
doc.flags.ignore_validate = True
doc.save()
self.validate_qty(doc)

View File

@@ -1170,7 +1170,11 @@ class update_entries_after:
diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
diff = flt(diff, self.flt_precision) # respect system precision
if diff < 0 and abs(diff) > 0.0001:
diff_threshold = 0.0001
if self.flt_precision > 4:
diff_threshold = 10 ** (-1 * self.flt_precision)
if diff < 0 and abs(diff) > diff_threshold:
# negative stock!
exc = sle.copy().update({"diff": diff})
self.exceptions.setdefault(sle.warehouse, []).append(exc)
@@ -2337,8 +2341,6 @@ def validate_reserved_batch_nos(kwargs):
{
"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,
"ignore_reserved_stock": True,
}