Compare commits

..

342 Commits

Author SHA1 Message Date
Frappe PR Bot
621558a30c chore(release): Bumped to Version 15.95.2
## [15.95.2](https://github.com/frappe/erpnext/compare/v15.95.1...v15.95.2) (2026-01-29)

### Bug Fixes

* **stock:** set incoming_rate with lcv rate for internal purchase ([6ea4f1a](6ea4f1a03d))
2026-01-29 12:50:23 +00:00
rohitwaghchaure
547fbec55f Merge pull request #52176 from frappe/mergify/bp/version-15/pr-52140
Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007) (backport #52140)
2026-01-29 18:17:43 +05:30
rohitwaghchaure
d9bd42965a Merge pull request #52195 from frappe/mergify/bp/version-15/pr-52191
Add Landed Cost Voucher Amount in Internal Purchase Receipt (backport #52158) (backport #52191)
2026-01-29 18:17:32 +05:30
kavin-114
419df361a7 test: add unit test to check internal purchase with lcv
(cherry picked from commit dd4fd89ef8)
(cherry picked from commit 3ccd1b4a6c)
2026-01-29 12:26:35 +00:00
kavin-114
6ea4f1a03d fix(stock): set incoming_rate with lcv rate for internal purchase
(cherry picked from commit f0dccc3cd7)
(cherry picked from commit 41c592a1a8)
2026-01-29 12:26:34 +00:00
mergify[bot]
65ed4e5cf6 Merge pull request #52140 from frappe/mergify/bp/version-15-hotfix/pr-52007
Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007)
(cherry picked from commit ad8c8cb0e8)
2026-01-29 09:01:32 +00:00
Frappe PR Bot
b268de4609 chore(release): Bumped to Version 15.95.1
## [15.95.1](https://github.com/frappe/erpnext/compare/v15.95.0...v15.95.1) (2026-01-28)

### Bug Fixes

* allow creation of DN in SI for items not having DN reference ([184fa88](184fa889c3))
* **asset capitalization:** update asset values using db_set ([74bf61e](74bf61e0c1))
* autofill warehouse for packed items ([0a87fa5](0a87fa5348))
* Bin reserved qty for production for extra material transfer ([b5d8477](b5d8477354))
* check the payment ledger entry has the dimension ([#51823](https://github.com/frappe/erpnext/issues/51823)) ([468ec80](468ec805f1))
* Ensure paid_amount is always numeric before calling allocate_amount_to_references (backport [#50935](https://github.com/frappe/erpnext/issues/50935)) ([#52035](https://github.com/frappe/erpnext/issues/52035)) ([9fce694](9fce694936))
* handle parent level project change ([7146c03](7146c0385c))
* handle undefined bank_transaction_mapping in quick entry ([d4195d3](d4195d31bf))
* job cards should not be deleted on close of WO ([8d06ee3](8d06ee3966))
* **journal-entry:** prevent submit failure due to double background queuing (backport [#52083](https://github.com/frappe/erpnext/issues/52083)) ([#52086](https://github.com/frappe/erpnext/issues/52086)) ([72a9b58](72a9b58b14))
* negative stock for purchae return ([f9fd0ff](f9fd0ffbae))
* **payment entry:** update currency symbol (backport [#51956](https://github.com/frappe/erpnext/issues/51956)) ([#52093](https://github.com/frappe/erpnext/issues/52093)) ([934b549](934b5494f0))
* **project:** add missing counter to project update naming series ([f61305a](f61305aa45))
* rejected qty in PR doesn't consider conversion factor ([83352b5](83352b5a34))
* **sales order:** set project at item level from parent ([a09b73e](a09b73e65d))
* **shipment:** user contact validation to use full name ([90dc22a](90dc22a57d))
* show message if image is removed from item description ([0c89cd5](0c89cd5524))
* **stock:** use purchase UOM in Supplier Quotation items ([dadd4b1](dadd4b1f95))
* strip whitespace in customer_name ([853faca](853facad96))
* swedish_address_template ([5e61922](5e6192249e))
* UOM of item not fetching in BOM ([14de520](14de520ebb))
* update country_wise_tax.json for Algerian Taxes (backport [#51878](https://github.com/frappe/erpnext/issues/51878)) ([#52037](https://github.com/frappe/erpnext/issues/52037)) ([d89ac99](d89ac99e76))
* validation to check at-least one raw material for manufacture entry ([650f874](650f874fbd))
2026-01-28 04:14:22 +00:00
ruthra kumar
44b726c2e3 Merge pull request #52104 from frappe/version-15-hotfix
chore: release v15
2026-01-28 09:43:01 +05:30
Mihir Kandoi
0c395725b7 Merge pull request #52123 from frappe/mergify/bp/version-15-hotfix/pr-51961
fix(sales order): set project at item level from parent (backport #51961)
2026-01-27 21:55:31 +05:30
SowmyaArunachalam
7146c0385c fix: handle parent level project change
(cherry picked from commit 543b6e51c0)
2026-01-27 16:24:06 +00:00
SowmyaArunachalam
e12564daa6 chore: use frappe.model.set_value
(cherry picked from commit 3b27f49d79)
2026-01-27 16:24:06 +00:00
SowmyaArunachalam
a09b73e65d fix(sales order): set project at item level from parent
(cherry picked from commit 9e51701e2a)
2026-01-27 16:24:05 +00:00
Mihir Kandoi
654a55260d Merge pull request #52121 from frappe/mergify/bp/version-15-hotfix/pr-52084
fix(shipment): user contact validation to use full name (backport #52084)
2026-01-27 21:28:34 +05:30
harrishragavan
90dc22a57d fix(shipment): user contact validation to use full name
(cherry picked from commit 3c6eb9a531)
2026-01-27 15:57:05 +00:00
Khushi Rawat
e826e03f9a Merge pull request #52073 from aerele/update-asset-purchase-amt
fix(asset capitalization): update asset values using db_set
2026-01-27 17:06:17 +05:30
ruthra kumar
de4e62e308 Merge pull request #52107 from frappe/mergify/bp/version-15-hotfix/pr-51823
fix: check the payment ledger entry has the dimension (backport #51823)
2026-01-27 16:27:32 +05:30
Vishnu Priya Baskaran
468ec805f1 fix: check the payment ledger entry has the dimension (#51823)
* fix: check the payment ledger entry has the dimension

* fix: add project in payment ledger entry

(cherry picked from commit efa3973b77)
2026-01-27 10:26:52 +00:00
Mihir Kandoi
cd8c6eac7c Merge pull request #52096 from frappe/mergify/bp/version-15-hotfix/pr-52088
fix: show message if image is removed from item description (backport #52088)
2026-01-27 14:56:03 +05:30
Mihir Kandoi
90d6bb34dc chore: resolve conflicts 2026-01-27 14:38:19 +05:30
Mihir Kandoi
1545904693 Merge pull request #52099 from aerele/support/fix--58134 2026-01-27 12:50:11 +05:30
Pandiyan37
dadd4b1f95 fix(stock): use purchase UOM in Supplier Quotation items 2026-01-27 12:28:07 +05:30
Mihir Kandoi
0c89cd5524 fix: show message if image is removed from item description
(cherry picked from commit b49c679a50)

# Conflicts:
#	erpnext/stock/doctype/item/item.py
2026-01-27 06:50:16 +00:00
mergify[bot]
934b5494f0 fix(payment entry): update currency symbol (backport #51956) (#52093)
Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com>
fix(payment entry): update currency symbol (#51956)
2026-01-27 06:32:59 +00:00
mergify[bot]
72a9b58b14 fix(journal-entry): prevent submit failure due to double background queuing (backport #52083) (#52086)
Co-authored-by: V Shankar <shankarv292002@gmail.com>
fix(journal-entry): prevent submit failure due to double background queuing (#52083)
2026-01-27 05:52:23 +00:00
Navin-S-R
5cfd8d1930 refactor: avoid multiple db_set 2026-01-26 23:06:37 +05:30
Navin-S-R
74bf61e0c1 fix(asset capitalization): update asset values using db_set 2026-01-26 21:17:06 +05:30
Mihir Kandoi
c4b135e1a2 Merge pull request #52065 from frappe/mergify/bp/version-15-hotfix/pr-52064
fix: strip whitespace in customer_name (backport #52064)
2026-01-26 15:30:50 +05:30
Shankarv19bcr
853facad96 fix: strip whitespace in customer_name
(cherry picked from commit e5ba0e6401)
2026-01-26 09:46:51 +00:00
ruthra kumar
636e1ac1f1 Merge pull request #52039 from frappe/mergify/bp/version-15-hotfix/pr-51670
fix: handle undefined bank_transaction_mapping in quick entry (backport #51670)
2026-01-25 13:11:45 +05:30
ruthra kumar
df996b8fd3 Merge pull request #52054 from frappe/mergify/bp/version-15-hotfix/pr-52050
fix: swedish_address_template (backport #52050)
2026-01-25 13:09:18 +05:30
mahsem
5e6192249e fix: swedish_address_template
(cherry picked from commit 334e8ada30)
2026-01-25 05:22:25 +00:00
rohitwaghchaure
398e8d00ec Merge pull request #52052 from frappe/mergify/bp/version-15-hotfix/pr-52043
fix: UOM of item not fetching in BOM (backport #52043)
2026-01-25 10:50:52 +05:30
rohitwaghchaure
6be30bbd71 Merge pull request #51904 from frappe/mergify/bp/version-15-hotfix/pr-51900
fix: validation to check at-least one raw material for manufacture entry (backport #51900)
2026-01-25 10:45:52 +05:30
Rohit Waghchaure
14de520ebb fix: UOM of item not fetching in BOM
(cherry picked from commit ba8eadda52)
2026-01-25 05:14:50 +00:00
rohitwaghchaure
770d0e7f7f Merge pull request #52030 from frappe/mergify/bp/version-15-hotfix/pr-52024
fix: Bin reserved qty for production for extra material transfer (backport #52024)
2026-01-25 10:43:48 +05:30
rohitwaghchaure
c351d6b1c0 chore: fix conflicts
Removed old implementation of make_serialized_item function and updated its definition.
2026-01-24 13:51:54 +05:30
rohitwaghchaure
a4b099e481 chore: fix conflicts
Removed subcontracting order validation methods from stock entry.
2026-01-24 13:50:33 +05:30
rohitwaghchaure
624ec19305 chore: fix conflicts
Remove test for reserved serial and batch items and clean up related code.
2026-01-24 13:42:04 +05:30
Abdeali Chharchhoda
e1c3125efa refactor: use console.error for error logging in Plaid integration
(cherry picked from commit 9322095786)
2026-01-24 07:07:32 +00:00
Abdeali Chharchhoda
d4195d31bf fix: handle undefined bank_transaction_mapping in quick entry
(cherry picked from commit 8a1b8259bd)
2026-01-24 07:07:32 +00:00
Abdeali Chharchhoda
f349be0a00 refactor: remove redundant onload function for bank mapping table
(cherry picked from commit 7c7ba0154a)
2026-01-24 07:07:31 +00:00
mergify[bot]
d89ac99e76 fix: update country_wise_tax.json for Algerian Taxes (backport #51878) (#52037)
fix: update country_wise_tax.json for Algerian Taxes (#51878)

* Algeria chart of accounts

Algeria chart of accounts

* Update Algeria Chart Of Account

* Algeria chart of account

* Algeria Chart of Account

Algeria Chart of Account

* Modify Algeria tax entries in country_wise_tax.json

Updated tax rates and account names for Algeria.

* Rename account for Algeria tax from VAT to TVA

Rename account for Algeria tax from VAT to TVA

(cherry picked from commit e810cd8440)

Co-authored-by: HALFWARE <contact@half-ware.com>
2026-01-24 06:48:04 +00:00
mergify[bot]
9fce694936 fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (backport #50935) (#52035)
fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (#50935)

fix: ensure paid_amount is not null in allocate_party_amount_against_ref_docs
(cherry picked from commit 50b3396064)

Co-authored-by: El-Shafei H. <el.shafei.developer@gmail.com>
2026-01-24 12:03:51 +05:30
Rohit Waghchaure
b5d8477354 fix: Bin reserved qty for production for extra material transfer
(cherry picked from commit f5378b6573)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2026-01-23 15:45:30 +00:00
rohitwaghchaure
0e3d276348 Merge pull request #52014 from frappe/mergify/bp/version-15-hotfix/pr-52006
fix: negative stock for purchase return (backport #52006)
2026-01-23 13:23:00 +05:30
rohitwaghchaure
3489b65f1a chore: fix conflicts 2026-01-23 12:27:37 +05:30
rohitwaghchaure
c8a52ec43c chore: fix conflicts
Removed deprecated method for batch-wise total available quantity and adjusted stock value calculations.
2026-01-23 11:51:02 +05:30
Rohit Waghchaure
f9fd0ffbae fix: negative stock for purchae return
(cherry picked from commit d68a04ad16)

# Conflicts:
#	erpnext/stock/serial_batch_bundle.py
2026-01-23 06:03:47 +00:00
rohitwaghchaure
69dc9e81d5 Merge pull request #52004 from frappe/mergify/bp/version-15-hotfix/pr-51989
fix: autofill warehouse for packed items (backport #51989)
2026-01-22 23:56:40 +05:30
Sudharsanan11
0a87fa5348 fix: autofill warehouse for packed items
(cherry picked from commit 3f8a0a4833)
2026-01-22 17:28:03 +00:00
Mihir Kandoi
81e7e96cb6 Merge pull request #51977 from frappe/mergify/bp/version-15-hotfix/pr-51967
fix(project): add missing counter to project update naming series (backport #51967)
2026-01-22 11:45:58 +05:30
mergify[bot]
f7770c3225 Merge pull request #51979 from frappe/mergify/bp/version-15-hotfix/pr-51966
fix(customer): add customer group filters (backport #51966)
2026-01-22 05:16:45 +00:00
ravibharathi656
f61305aa45 fix(project): add missing counter to project update naming series
(cherry picked from commit 49e64f4e1c)
2026-01-22 04:52:56 +00:00
Mihir Kandoi
113a6e079a Merge pull request #51971 from frappe/mergify/bp/version-15-hotfix/pr-51968 2026-01-22 09:04:48 +05:30
mergify[bot]
c35426b9f9 Merge pull request #51969 from frappe/mergify/bp/version-15-hotfix/pr-51964
fix: create DN btn should not be shown if it cannot be created (backport #51964)
2026-01-21 17:27:37 +00:00
Mihir Kandoi
83352b5a34 fix: rejected qty in PR doesn't consider conversion factor
(cherry picked from commit 343ee9695b)
2026-01-21 17:20:45 +00:00
Mihir Kandoi
e54bb0da69 Merge pull request #51959 from frappe/mergify/bp/version-15-hotfix/pr-51947
fix: job cards should not be deleted on close of WO (backport #51947)
2026-01-21 16:02:01 +05:30
Mihir Kandoi
8d06ee3966 fix: job cards should not be deleted on close of WO
(cherry picked from commit c919b1de38)
2026-01-21 10:17:00 +00:00
Mihir Kandoi
6b4101d202 Merge pull request #51925 from frappe/mergify/bp/version-15-hotfix/pr-51909
fix: allow creation of DN in SI for items not having DN reference (backport #51909)
2026-01-21 15:41:21 +05:30
Mihir Kandoi
386567a6ea chore: resolve conflicts 2026-01-21 15:27:12 +05:30
Mihir Kandoi
d3440cf545 chore: resolve conflicts 2026-01-21 15:24:14 +05:30
mergify[bot]
11544818f1 Merge pull request #51950 from frappe/mergify/bp/version-15-hotfix/pr-51948
fix: warehouse permissions in MR incorrectly ignored (backport #51948)
2026-01-21 08:51:41 +00:00
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
Mihir Kandoi
184fa889c3 fix: allow creation of DN in SI for items not having DN reference
(cherry picked from commit b691de0147)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.js
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
2026-01-20 12:14:43 +00:00
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
Rohit Waghchaure
650f874fbd fix: validation to check at-least one raw material for manufacture entry
(cherry picked from commit f003b3c378)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
2026-01-20 08:25:57 +00:00
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
Frappe PR Bot
0b565026a4 chore(release): Bumped to Version 15.94.0
# [15.94.0](https://github.com/frappe/erpnext/compare/v15.93.2...v15.94.0) (2026-01-07)

### Bug Fixes

* add company filters to project ([d6511b0](d6511b0045))
* **journal entry:** use submission_queue to perform submit and cancel actions for rows over 100 ([1d58e9b](1d58e9b91a))
* not able to submit backdated stock reco ([4b60979](4b6097914a))
* precision issue causing reservation error ([2d49cc9](2d49cc9ab2))
* resolve conflict ([dbd2964](dbd2964139))
* SABB not cancelled on cancel of Stock Reco ([eebd885](eebd88529f))
* **stock:** prevent excess stock reservation ([4d31012](4d31012df2))
* **stock:** remove item image to avoid setting the image of previous item ([6f1cfdb](6f1cfdb1de))
* **trial balance party:** add check for parties with zero credit and debit ([a0566c9](a0566c9e98))
* update filters on period closing voucher ([728a8b0](728a8b0b7d))

### Features

* add default-age-range in accounts settings (backport [#51458](https://github.com/frappe/erpnext/issues/51458)) ([#51531](https://github.com/frappe/erpnext/issues/51531)) ([582db48](582db48ca5))
* allow data import for asset repair doctype ([dc10ef4](dc10ef4287))
2026-01-07 05:01:14 +00:00
ruthra kumar
5fcf5d58f0 Merge pull request #51538 from frappe/version-15-hotfix
chore: release v15
2026-01-07 10:29:48 +05:30
rohitwaghchaure
fac865a1b4 Merge branch 'version-15' into version-15-hotfix 2026-01-07 09:58:13 +05:30
kavin-114
d3f2da0d59 fix: remove posting date & time on SRE batch validation 2026-01-07 01:00:29 +05:30
ruthra kumar
f8fb58feaf Merge pull request #51493 from frappe/mergify/bp/version-15-hotfix/pr-51326
fix(journal entry): use submission_queue to perform submit and cancel actions for rows over 100 (backport #51326)
2026-01-06 20:57:12 +05:30
ruthra kumar
9bba78f7a2 Merge pull request #51545 from frappe/mergify/bp/version-15-hotfix/pr-51424
fix(trial balance party): add check for parties with zero credit and debit (backport #51424)
2026-01-06 18:03:13 +05:30
Jatin3128
a0566c9e98 fix(trial balance party): add check for parties with zero credit and debit
(cherry picked from commit 83ddaf1696)
2026-01-06 12:18:06 +00:00
Khushi Rawat
64a7e3f683 Merge pull request #51541 from frappe/mergify/bp/version-15-hotfix/pr-51540
feat: allow data import for asset repair doctype (backport #51540)
2026-01-06 17:01:12 +05:30
Khushi Rawat
dbd2964139 fix: resolve conflict 2026-01-06 16:46:20 +05:30
khushi8112
dc10ef4287 feat: allow data import for asset repair doctype
(cherry picked from commit 49f1688a51)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/asset_repair.json
2026-01-06 10:39:53 +00:00
Nishka Gosalia
3cc41cf643 fix: correct uom reflecting in sales order when fetching from barcode 2026-01-06 12:51:47 +05:30
ruthra kumar
582db48ca5 feat: add default-age-range in accounts settings (backport #51458) (#51531)
Merge pull request #51458 from aerele/default-age-range

feat: add default-age-range in accounts settings
(cherry picked from commit f8f82ccf31)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json

Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com>
2026-01-06 12:43:35 +05:30
Sowmya
0452820ab0 Merge pull request #51458 from aerele/default-age-range
feat: add default-age-range in accounts settings
(cherry picked from commit f8f82ccf31)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2026-01-06 11:33:25 +05:30
Navin-S-R
1d58e9b91a fix(journal entry): use submission_queue to perform submit and cancel actions for rows over 100
(cherry picked from commit fa8e80c6a0)
2026-01-05 06:52:05 +00:00
ruthra kumar
7bbcafed8d Merge pull request #51489 from frappe/mergify/bp/version-15-hotfix/pr-51457
fix: add company filters to project (backport #51457)
2026-01-05 11:19:33 +05:30
SowmyaArunachalam
d6511b0045 fix: add company filters to project
(cherry picked from commit 7c16db567b)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.js
2026-01-05 11:12:44 +05:30
ruthra kumar
0d790a6cd5 Merge pull request #51487 from frappe/mergify/bp/version-15-hotfix/pr-51467
fix: update filters on period closing voucher (backport #51467)
2026-01-05 10:53:31 +05:30
SowmyaArunachalam
728a8b0b7d fix: update filters on period closing voucher
(cherry picked from commit 7ab1e1f677)
2026-01-05 05:20:32 +00:00
rohitwaghchaure
d8cb65e440 Merge pull request #51477 from frappe/mergify/bp/version-15-hotfix/pr-51475
fix: SABB not cancelled on cancel of Stock Reco (backport #51475)
2026-01-03 16:34:11 +05:30
Rohit Waghchaure
eebd88529f fix: SABB not cancelled on cancel of Stock Reco
(cherry picked from commit b204853193)
2026-01-03 10:47:17 +00:00
Frappe PR Bot
1740fce6c8 chore(release): Bumped to Version 15.93.2
## [15.93.2](https://github.com/frappe/erpnext/compare/v15.93.1...v15.93.2) (2026-01-03)

### Bug Fixes

* not able to submit backdated stock reco ([9ef7d45](9ef7d45486))
2026-01-03 10:31:34 +00:00
rohitwaghchaure
a01dc0e205 Merge pull request #51471 from frappe/mergify/bp/version-15/pr-51470
fix: not able to submit backdated stock reco (backport #51468) (backport #51470)
2026-01-03 15:59:59 +05:30
rohitwaghchaure
1ec2cc3820 chore: fix conflicts
Removed unused on_discard method and cleaned up code.

(cherry picked from commit 46f3ab1c39)
2026-01-03 10:12:09 +00:00
Rohit Waghchaure
9ef7d45486 fix: not able to submit backdated stock reco
(cherry picked from commit cccd34b06a)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
(cherry picked from commit 4b6097914a)
2026-01-03 10:12:09 +00:00
rohitwaghchaure
02203ca534 Merge pull request #51470 from frappe/mergify/bp/version-15-hotfix/pr-51468
fix: not able to submit backdated stock reco (backport #51468)
2026-01-03 15:41:07 +05:30
rohitwaghchaure
46f3ab1c39 chore: fix conflicts
Removed unused on_discard method and cleaned up code.
2026-01-03 15:23:29 +05:30
Rohit Waghchaure
4b6097914a fix: not able to submit backdated stock reco
(cherry picked from commit cccd34b06a)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
2026-01-03 09:52:09 +00:00
rohitwaghchaure
decc27a446 Merge pull request #51411 from rohitwaghchaure/backport-4972
fix: precision issue causing reservation error
2026-01-01 09:51:50 +05:30
Rohit Waghchaure
2d49cc9ab2 fix: precision issue causing reservation error 2025-12-31 13:29:08 +05:30
rohitwaghchaure
5955b699c3 Merge pull request #51394 from frappe/mergify/bp/version-15-hotfix/pr-51375
fix(stock): prevent excess stock reservation (backport #51375)
2025-12-31 11:09:14 +05:30
rohitwaghchaure
c8d8ec91c4 Merge pull request #51395 from frappe/mergify/bp/version-15-hotfix/pr-51341
fix(stock): remove item image to avoid setting the image of previous item (backport #51341)
2025-12-31 11:04:29 +05:30
Frappe PR Bot
a857923853 chore(release): Bumped to Version 15.93.1
## [15.93.1](https://github.com/frappe/erpnext/compare/v15.93.0...v15.93.1) (2025-12-30)

### Bug Fixes

* **accounts-payable-summary:** add Show GL Balance check similar to A… (backport [#50802](https://github.com/frappe/erpnext/issues/50802)) ([#50805](https://github.com/frappe/erpnext/issues/50805)) ([a04f560](a04f560048))
* **bank reconciliation tool:** carry bank account to payment entry ([cd930c0](cd930c05b8))
* **bank reconciliation tool:** fix incorrect bank account field mapping ([9ef0e8b](9ef0e8beb7))
* expense_account query override in Purchase Receipt ([6f3904a](6f3904a20a))
* **payment entry:** clear party_name for internal transfer ([431e687](431e68741b))
* prevent reuse of serial no in manufacture and repack entry ([24f6f1e](24f6f1e434))
* **repost accounting ledger:** prevent preview generation when no vouchers are selected ([93c1a3f](93c1a3f8f3))
* start reposting accounting ledger after commit ([e6acdf3](e6acdf36e2))
* **stock:** remove total bar in chart view ([d9888d5](d9888d5195))
* updating base amounts through python for timesheet for v15 ([9d2e0f6](9d2e0f67d5))
* validate depreciation row values ([2f10b9c](2f10b9c510))
* validate party's existing transaction currency before merging ([1c40a61](1c40a61d23))

### Performance Improvements

* composite index for serial no ([507a561](507a561922))
* index for warehouse field ([4753594](4753594a26))
2025-12-30 13:35:04 +00:00
ruthra kumar
4f6499836e Merge pull request #51391 from frappe/version-15-hotfix
chore: release v15
2025-12-30 19:03:35 +05:30
Sudharsanan11
6f1cfdb1de fix(stock): remove item image to avoid setting the image of previous item
(cherry picked from commit 69e94248c1)
2025-12-30 11:21:11 +00:00
Sudharsanan11
4d31012df2 fix(stock): prevent excess stock reservation
(cherry picked from commit e1f9adf4e9)
2025-12-30 11:20:11 +00:00
ruthra kumar
944dacc12f Merge pull request #51393 from frappe/mergify/bp/version-15-hotfix/pr-51340
fix(bank reconciliation tool): carry bank account to payment entry (backport #51340)
2025-12-30 16:28:52 +05:30
ravibharathi656
9ef0e8beb7 fix(bank reconciliation tool): fix incorrect bank account field mapping
(cherry picked from commit 9dfb0fdcbb)
2025-12-30 10:28:40 +00:00
ravibharathi656
cd930c05b8 fix(bank reconciliation tool): carry bank account to payment entry
(cherry picked from commit 6fc9636642)
2025-12-30 10:28:39 +00:00
ruthra kumar
c42aa4f89b Merge pull request #51384 from frappe/mergify/bp/version-15-hotfix/pr-51368
fix: start reposting accounting ledger after commit (backport #51368)
2025-12-30 14:08:31 +05:30
Khushi Rawat
3f4ffcc955 Merge pull request #51387 from frappe/mergify/bp/version-15-hotfix/pr-51380
fix: expense_account query override in Purchase Receipt (backport #51380)
2025-12-30 13:00:18 +05:30
khushi8112
6f3904a20a fix: expense_account query override in Purchase Receipt
(cherry picked from commit 292a51c160)
2025-12-30 07:27:01 +00:00
Ponnusamy
e6acdf36e2 fix: start reposting accounting ledger after commit
(cherry picked from commit 469a1ade79)
2025-12-30 06:53:18 +00:00
ruthra kumar
9ee40351c5 Merge pull request #51378 from frappe/mergify/bp/version-15-hotfix/pr-51361
fix(payment entry): clear party_name for internal transfer (backport #51361)
2025-12-30 12:02:43 +05:30
ravibharathi656
431e68741b fix(payment entry): clear party_name for internal transfer
(cherry picked from commit aae0448e1f)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js
2025-12-30 11:53:48 +05:30
ruthra kumar
f1c98df7bb Merge pull request #51372 from frappe/mergify/bp/version-15-hotfix/pr-51171
fix: validate party's existing transaction currency before merging (backport #51171)
2025-12-30 11:07:34 +05:30
Nabin Hait
1c40a61d23 fix: validate party's existing transaction currency before merging
(cherry picked from commit f48b90c600)
2025-12-30 04:52:39 +00:00
Mihir Kandoi
fdf80a6d02 Merge pull request #51319 from nishkagosalia/gh-50389-v15 2025-12-30 10:19:39 +05:30
rohitwaghchaure
b9c123bd89 Merge pull request #51350 from frappe/mergify/bp/version-15-hotfix/pr-50363
fix: prevent reuse of serial no in manufacture and repack entry (backport #50363)
2025-12-28 10:45:51 +05:30
Rohit Waghchaure
24f6f1e434 fix: prevent reuse of serial no in manufacture and repack entry
(cherry picked from commit 48b537dc8c)
2025-12-28 04:57:30 +00:00
Mihir Kandoi
f263a7f65c Merge pull request #51344 from frappe/mergify/bp/version-15-hotfix/pr-51330
fix(stock): remove total bar in chart view (backport #51330)
2025-12-26 22:04:12 +05:30
Sudharsanan11
d9888d5195 fix(stock): remove total bar in chart view
(cherry picked from commit 7df349844a)
2025-12-26 16:19:50 +00:00
rohitwaghchaure
52b3740eb1 Merge pull request #51329 from frappe/mergify/bp/version-15-hotfix/pr-51322
perf: composite index for serial no (backport #51322)
2025-12-25 15:43:25 +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
Rohit Waghchaure
507a561922 perf: composite index for serial no
(cherry picked from commit 734d553338)
2025-12-25 03:40:05 +00:00
Mihir Kandoi
88e305f5a2 Merge pull request #51323 from frappe/mergify/bp/version-15-hotfix/pr-50826 2025-12-24 21:51:11 +05:30
Nishka Gosalia
9d2e0f67d5 fix: updating base amounts through python for timesheet for v15 2025-12-24 21:44:58 +05:30
Abdeali Chharchhoda
fd718833b1 refactor: optimize picked quantity updates using bulk_update
(cherry picked from commit 5f986e4032)
2025-12-24 16:06:19 +00:00
rohitwaghchaure
c7c938c259 Merge pull request #51313 from frappe/mergify/bp/version-15-hotfix/pr-51310
perf: index for warehouse field (backport #51310)
2025-12-24 15:30:23 +05:30
rohitwaghchaure
35ae839ab7 chore: fix conflicts 2025-12-24 15:06:48 +05:30
Rohit Waghchaure
4753594a26 perf: index for warehouse field
(cherry picked from commit 23c70332df)

# Conflicts:
#	erpnext/stock/doctype/serial_no/serial_no.json
2025-12-24 09:29:33 +00:00
Khushi Rawat
4e4d2cefda Merge pull request #51282 from khushi8112/validate-finance-books-row-values
fix: validate finance books row values
2025-12-24 14:52:15 +05:30
mergify[bot]
a04f560048 fix(accounts-payable-summary): add Show GL Balance check similar to A… (backport #50802) (#50805) 2025-12-24 13:11:28 +05:30
Diptanil Saha
c9d8c5b419 Merge pull request #51307 from frappe/mergify/bp/version-15-hotfix/pr-51304
fix(repost accounting ledger): prevent preview generation when no vouchers are selected (backport #51304)
2025-12-24 13:09:32 +05:30
diptanilsaha
93c1a3f8f3 fix(repost accounting ledger): prevent preview generation when no vouchers are selected
(cherry picked from commit bd9f5fca08)
2025-12-24 07:24:35 +00:00
Frappe PR Bot
926b4c7065 chore(release): Bumped to Version 15.93.0
# [15.93.0](https://github.com/frappe/erpnext/compare/v15.92.5...v15.93.0) (2025-12-23)

### Bug Fixes

* added limit ([73643de](73643de612))
* **buying:** add disabled filter for supplier ([0b6b73b](0b6b73b500))
* cascade projected quantity across multiple items in material requests ([dffd5d9](dffd5d9cdd))
* de-duplicate rows on disassembly with multiple manufacture entries ([68eeba4](68eeba41c1))
* do not hide primary-action for composite asset ([cbcfe6e](cbcfe6ec36))
* don't fetch qty as it's unused ([dd19b95](dd19b95113))
* incorrect current qty in stock reco (backport [#51152](https://github.com/frappe/erpnext/issues/51152)) ([#51158](https://github.com/frappe/erpnext/issues/51158)) ([89d6a8f](89d6a8f02e))
* limit condition to fetch serial nos ([425dcee](425dcee5bf))
* **manufacturing:** validate delivered qty in production plan ([c01f20d](c01f20da00))
* **payment entry:** set row id for 'On Previous Row Amount' or 'On Previous Row Total' charge type on tax table ([d7c50cf](d7c50cfa7c))
* **pegged currencies:** skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport [#51188](https://github.com/frappe/erpnext/issues/51188)) ([#51203](https://github.com/frappe/erpnext/issues/51203)) ([8ef09c0](8ef09c0dc0))
* same serial number was picked in multiple sales invoices ([dc5faa8](dc5faa8b71))
* show company currency in asset depreciation schedule ([5b1795b](5b1795b0a5))
* **stock-report:** ignore reserved stock in batch qty calculation ([26a36d8](26a36d807e))
* **stock:** handle serial and batch nos for disassemble stock entry ([59aef4f](59aef4fc8c))
* **stock:** ignore reserved stock while calculating batch qty ([ac2402d](ac2402dd2a))
* support disassemble of RMs other than in BOM ([72d77a5](72d77a5e99))
* update batch_qty using get_batch_qty ([ca835c8](ca835c831b))
* use get_batch_qty to fetch batch data ([10b0da8](10b0da8bc8))
* use original logic for v15 - inverted wrt v16 ([0452b22](0452b22aa6))
* use serial and batch bundle to fetch incoming rate (backport [#51119](https://github.com/frappe/erpnext/issues/51119)) ([#51146](https://github.com/frappe/erpnext/issues/51146)) ([2d42904](2d42904bfb))
* use stock adjustment if the account has not set ([8a01a70](8a01a709a7))

### Features

* add redirect button on report ([fe80d1d](fe80d1d0e7))
* **report:** add batch qty update functionality in report ([57c356a](57c356a1cd))

### Performance Improvements

* optimize company monthly sales query using date range ([#48942](https://github.com/frappe/erpnext/issues/48942)) ([0488658](048865811c))
2025-12-23 16:42:06 +00:00
Mihir Kandoi
f29ad04eab Merge pull request #51280 from frappe/version-15-hotfix 2025-12-23 22:10:38 +05:30
Mihir Kandoi
8fa73b370a Merge pull request #51299 from frappe/mergify/bp/version-15-hotfix/pr-51256
fix(manufacturing): validate delivered qty in production plan (backport #51256)
2025-12-23 21:47:36 +05:30
Sudharsanan11
2645bf648d test(manufacturing): add test to validate planned qty
(cherry picked from commit 2073cb0106)
2025-12-23 16:02:09 +00:00
Sudharsanan11
c01f20da00 fix(manufacturing): validate delivered qty in production plan
(cherry picked from commit eda8a621c6)
2025-12-23 16:02:09 +00:00
Mihir Kandoi
d3f434b803 Merge pull request #51177 from aerele/fix/disassemble-serial-and-batch-bundle 2025-12-23 21:07:32 +05:30
Mihir Kandoi
0687b035b5 Merge pull request #51296 from frappe/mergify/bp/version-15-hotfix/pr-51225 2025-12-23 21:02:54 +05:30
Mihir Kandoi
04a98b2b64 Merge pull request #51142 from frappe/mergify/bp/version-15-hotfix/pr-51141
fix(buying): add disabled filter for supplier (backport #51141)
2025-12-23 20:50:26 +05:30
Mihir Kandoi
eae1886043 Merge branch 'mergify/bp/version-15-hotfix/pr-51141' of https://github.com/frappe/erpnext into mergify/bp/version-15-hotfix/pr-51141 2025-12-23 20:47:48 +05:30
Mihir Kandoi
5f295c5310 chore: resolve conflicts 2025-12-23 20:47:16 +05:30
SowmyaArunachalam
fe80d1d0e7 feat: add redirect button on report
(cherry picked from commit c0ac5f94b5)
2025-12-23 15:16:40 +00:00
Mihir Kandoi
5e7b674ee4 Merge pull request #51250 from frappe/mergify/bp/version-15-hotfix/pr-51215
fix: de-duplicate rows on disassembly with multiple manufacture entries (backport #51215)
2025-12-23 20:28:37 +05:30
rohitwaghchaure
4166c7ff47 Merge branch 'version-15' into version-15-hotfix 2025-12-23 18:15:55 +05:30
mergify[bot]
0f2fb54756 Merge pull request #51292 from frappe/mergify/bp/version-15-hotfix/pr-51285
fix(patch): handle currency exchange settings frankfurter api update for older versions (backport #51285)
2025-12-23 18:00:19 +05:30
rohitwaghchaure
9409155594 Merge pull request #51289 from frappe/mergify/bp/version-15-hotfix/pr-51276
fix: use stock adjustment if the account has not set (backport #51276)
2025-12-23 17:38:02 +05:30
Rohit Waghchaure
8a01a709a7 fix: use stock adjustment if the account has not set
(cherry picked from commit 9bbcbe0ac3)
2025-12-23 11:38:10 +00:00
Khushi Rawat
cc1f38010d Merge pull request #51284 from khushi8112/do-not-disable-primary-action-button-bp-51205
fix: do not hide primary-action for composite asset
2025-12-23 16:43:51 +05:30
Smit Vora
f0aefa4274 chore: v15 compatible get-all query 2025-12-23 16:22:06 +05:30
khushi8112
cbcfe6ec36 fix: do not hide primary-action for composite asset 2025-12-23 16:02:50 +05:30
khushi8112
6ff002dbe3 refactor: split long function into smaller 2025-12-23 15:44:43 +05:30
khushi8112
2f10b9c510 fix: validate depreciation row values 2025-12-23 15:32:02 +05:30
Khushi Rawat
83b1e037cb Merge pull request #51198 from aerele/repost-asset-sales-voucher
fix: avoid creating multiple asset depreciations while reposting asset sales invoice
2025-12-23 14:16:20 +05:30
Frappe PR Bot
5007abf7ae chore(release): Bumped to Version 15.92.5
## [15.92.5](https://github.com/frappe/erpnext/compare/v15.92.4...v15.92.5) (2025-12-23)

### Bug Fixes

* bumped version ([6df222a](6df222a1ca))
2025-12-23 08:16:22 +00:00
rohitwaghchaure
d31dd1a023 Merge pull request #51277 from rohitwaghchaure/fixed-bumped-version-v15
fix: bumped version
2025-12-23 13:44:58 +05:30
Rohit Waghchaure
6df222a1ca fix: bumped version 2025-12-23 13:27:08 +05:30
rohitwaghchaure
265da1056d Merge pull request #51272 from rohitwaghchaure/fixed-bumped-version
chore(release): Bumped to Version 15.92.5
2025-12-23 12:59:54 +05:30
Rohit Waghchaure
670beae048 chore(release): Bumped to Version 15.92.5 2025-12-23 12:49:12 +05:30
rohitwaghchaure
6b2a077bec Merge pull request #51270 from frappe/mergify/bp/version-15/pr-51260
Revert "fix: performance of the reposting" (backport #51258) (backport #51260)
2025-12-23 12:37:30 +05:30
rohitwaghchaure
43831e9785 chore: fix linters issue
(cherry picked from commit e9c37642c8)
(cherry picked from commit c095938e69)
2025-12-23 06:49:20 +00:00
rohitwaghchaure
7187992170 chore: fix test case
(cherry picked from commit d191b80587)
(cherry picked from commit aefde87a0c)
2025-12-23 06:49:19 +00:00
rohitwaghchaure
f3d0a91fb3 Revert "fix: performance of the reposting"
(cherry picked from commit 280558efa2)
(cherry picked from commit c89fe9f1ca)
2025-12-23 06:49:19 +00:00
rohitwaghchaure
f318a3658d Merge pull request #51265 from frappe/mergify/bp/version-15-hotfix/pr-51251
fix: order by to fetch serial nos
2025-12-22 18:23:49 +05:30
rohitwaghchaure
8f52f14505 chore: fix conflicts
Refactor based_on retrieval method and remove unused fields.
2025-12-22 18:04:47 +05:30
Rohit Waghchaure
425dcee5bf fix: limit condition to fetch serial nos
(cherry picked from commit da4b78491d)

# Conflicts:
#	erpnext/stock/get_item_details.py
2025-12-22 12:16:04 +00:00
rohitwaghchaure
06aded08ae Merge pull request #51260 from frappe/mergify/bp/version-15-hotfix/pr-51258
Revert "fix: performance of the reposting" (backport #51258)
2025-12-22 17:15:12 +05:30
rohitwaghchaure
c095938e69 chore: fix linters issue
(cherry picked from commit e9c37642c8)
2025-12-22 11:12:50 +00:00
rohitwaghchaure
aefde87a0c chore: fix test case
(cherry picked from commit d191b80587)
2025-12-22 11:12:50 +00:00
rohitwaghchaure
c89fe9f1ca Revert "fix: performance of the reposting"
(cherry picked from commit 280558efa2)
2025-12-22 11:12:50 +00:00
Frappe PR Bot
658a7c536d chore(release): Bumped to Version 15.92.4
## [15.92.4](https://github.com/frappe/erpnext/compare/v15.92.3...v15.92.4) (2025-12-22)

### Bug Fixes

* added limit ([0e73c12](0e73c12add))
* same serial number was picked in multiple sales invoices ([05ad50f](05ad50f98b))
2025-12-22 08:35:29 +00:00
rohitwaghchaure
94c430cc6e Merge pull request #51254 from frappe/mergify/bp/version-15/pr-51247
fix: same serial number was picked in multiple sales invoices (backport #51244) (backport #51247)
2025-12-22 14:04:07 +05:30
rohitwaghchaure
0e73c12add fix: added limit
(cherry picked from commit 73643de612)
2025-12-22 07:38:26 +00:00
rohitwaghchaure
aba3d7821c chore: fix conflicts
Removed logic for handling reserved serial numbers in sales invoices.

(cherry picked from commit c77c426652)
2025-12-22 07:38:25 +00:00
Rohit Waghchaure
05ad50f98b fix: same serial number was picked in multiple sales invoices
(cherry picked from commit 61c31f0cd0)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
(cherry picked from commit dc5faa8b71)
2025-12-22 07:38:25 +00:00
rohitwaghchaure
5200739d7b Merge pull request #51247 from frappe/mergify/bp/version-15-hotfix/pr-51244
fix: same serial number was picked in multiple sales invoices (backport #51244)
2025-12-22 13:07:47 +05:30
rohitwaghchaure
73643de612 fix: added limit 2025-12-22 12:02:38 +05:30
rohitwaghchaure
fab49e41a6 Merge pull request #51245 from frappe/mergify/bp/version-15-hotfix/pr-51242
fix(stock-report): ignore reserved stock in batch qty calculation (backport #51242)
2025-12-22 11:46:51 +05:30
Smit Vora
bb00bb83f8 test: ensure full qty reversal for items outside of BOM on disassemble
(cherry picked from commit 5b3d2c0d02)
2025-12-22 06:09:08 +00:00
Smit Vora
72d77a5e99 fix: support disassemble of RMs other than in BOM
(cherry picked from commit ce123f1a89)
2025-12-22 06:09:07 +00:00
Smit Vora
16112630ea test: ensure no regression after save and submit on disassemble
(cherry picked from commit 18ac589796)
2025-12-22 06:09:07 +00:00
Smit Vora
dd19b95113 fix: don't fetch qty as it's unused
(cherry picked from commit df13308663)
2025-12-22 06:09:07 +00:00
Smit Vora
68eeba41c1 fix: de-duplicate rows on disassembly with multiple manufacture entries
(cherry picked from commit a091e47bd7)
2025-12-22 06:09:06 +00:00
rohitwaghchaure
c77c426652 chore: fix conflicts
Removed logic for handling reserved serial numbers in sales invoices.
2025-12-22 11:37:19 +05:30
Rohit Waghchaure
dc5faa8b71 fix: same serial number was picked in multiple sales invoices
(cherry picked from commit 61c31f0cd0)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
2025-12-22 04:09:59 +00:00
Pugazhendhi Velu
26a36d807e fix(stock-report): ignore reserved stock in batch qty calculation
(cherry picked from commit 9a1f551e53)
2025-12-21 16:57:51 +00:00
rohitwaghchaure
432b33ac5f Merge pull request #51223 from frappe/mergify/bp/version-15-hotfix/pr-49951
feat(report): add batch qty update functionality in report (backport #49951)
2025-12-21 22:26:22 +05:30
Pugazhendhi Velu
ca835c831b fix: update batch_qty using get_batch_qty
(cherry picked from commit 15d9d8b719)
2025-12-19 14:56:18 +00:00
Pugazhendhi Velu
10b0da8bc8 fix: use get_batch_qty to fetch batch data
(cherry picked from commit cf03d03033)
2025-12-19 14:56:18 +00:00
Pugazhendhi Velu
e7fcacbe69 refactor: fetch batch qty difference in a single db query
(cherry picked from commit 9cc77934a6)
2025-12-19 14:56:18 +00:00
Pugazhendhi Velu
57c356a1cd feat(report): add batch qty update functionality in report
(cherry picked from commit f40c492a05)
2025-12-19 14:56:18 +00:00
Frappe PR Bot
6e7de0ac47 chore(release): Bumped to Version 15.92.3
## [15.92.3](https://github.com/frappe/erpnext/compare/v15.92.2...v15.92.3) (2025-12-19)

### Bug Fixes

* **stock:** ignore reserved stock while calculating batch qty ([35478bb](35478bbf91))
2025-12-19 13:25:33 +00:00
rohitwaghchaure
2277b1aff5 Merge pull request #51221 from frappe/mergify/bp/version-15/pr-51220
fix(stock): ignore reserved stock while calculating batch qty (backport #51214) (backport #51220)
2025-12-19 18:54:08 +05:30
rohitwaghchaure
58c793f14e chore: fix conflicts
Removed logic for handling reserved stock when calculating batch quantity.

(cherry picked from commit 9ade0725e8)
2025-12-19 12:55:11 +00:00
Sudharsanan11
08cd08adcd test(stock): add test for ignore reserve stock
(cherry picked from commit 4d8ec5f54c)
(cherry picked from commit b20405dbf2)
2025-12-19 12:55:11 +00:00
Sudharsanan11
35478bbf91 fix(stock): ignore reserved stock while calculating batch qty
(cherry picked from commit b23c6e2687)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
(cherry picked from commit ac2402dd2a)
2025-12-19 12:55:11 +00:00
rohitwaghchaure
40481508f1 Merge pull request #51220 from frappe/mergify/bp/version-15-hotfix/pr-51214
fix(stock): ignore reserved stock while calculating batch qty (backport #51214)
2025-12-19 18:24:37 +05:30
rohitwaghchaure
9ade0725e8 chore: fix conflicts
Removed logic for handling reserved stock when calculating batch quantity.
2025-12-19 18:07:42 +05:30
Sudharsanan11
b20405dbf2 test(stock): add test for ignore reserve stock
(cherry picked from commit 4d8ec5f54c)
2025-12-19 12:31:09 +00:00
Sudharsanan11
ac2402dd2a fix(stock): ignore reserved stock while calculating batch qty
(cherry picked from commit b23c6e2687)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
2025-12-19 12:31:09 +00:00
Smit Vora
c827fc3259 Merge pull request #51103 from frappe/mergify/bp/version-15-hotfix/pr-50788
fix: cascade projected quantity across multiple items in material requests (backport #50788)
2025-12-19 14:07:50 +05:30
Smit Vora
f13db03c9b test: make corrections to tests based on v15 functionality 2025-12-19 10:33:44 +05:30
Frappe PR Bot
54ed428225 chore(release): Bumped to Version 15.92.2
## [15.92.2](https://github.com/frappe/erpnext/compare/v15.92.1...v15.92.2) (2025-12-18)

### Bug Fixes

* **pegged currencies:** skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport [#51188](https://github.com/frappe/erpnext/issues/51188)) ([#51203](https://github.com/frappe/erpnext/issues/51203)) ([195f902](195f90232d))
2025-12-18 12:38:51 +00:00
Diptanil Saha
c046dad2c3 Merge pull request #51206 from frappe/mergify/bp/version-15/pr-51203
fix(pegged currencies): skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport #51188)
2025-12-18 18:07:29 +05:30
mergify[bot]
195f90232d fix(pegged currencies): skip adding currencies_to_add items on pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport #51188) (#51203)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pegged currencies): skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (#51188)

(cherry picked from commit 8ef09c0dc0)
2025-12-18 12:18:08 +00:00
mergify[bot]
8ef09c0dc0 fix(pegged currencies): skip adding currencies_to_add items on pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport #51188) (#51203)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pegged currencies): skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (#51188)
2025-12-18 17:05:39 +05:30
Mihir Kandoi
7f91f95f95 chore: resolve conflicts 2025-12-18 15:37:23 +05:30
Navin-S-R
696a0892fa refactor: improve asset depreciation handling during asset sales 2025-12-18 13:27:06 +05:30
Sudharsanan11
59aef4fc8c fix(stock): handle serial and batch nos for disassemble stock entry 2025-12-17 16:16:54 +05:30
Khushi Rawat
99cd7cf63e Merge pull request #51164 from frappe/mergify/bp/version-15-hotfix/pr-51156
fix: show company currency in asset depreciation schedule (backport #51156)
2025-12-17 15:51:27 +05:30
Diptanil Saha
44082cae72 Merge pull request #51170 from frappe/mergify/bp/version-15-hotfix/pr-51169
fix(payment entry): set row id for 'On Previous Row Amount' or 'On Previous Row Total' charge type on tax table (backport #51169)
2025-12-17 15:32:05 +05:30
diptanilsaha
d7c50cfa7c fix(payment entry): set row id for 'On Previous Row Amount' or 'On Previous Row Total' charge type on tax table
(cherry picked from commit 848f8d6b1f)
2025-12-17 09:52:54 +00:00
Frappe PR Bot
1e52738150 chore(release): Bumped to Version 15.92.1
## [15.92.1](https://github.com/frappe/erpnext/compare/v15.92.0...v15.92.1) (2025-12-17)

### Bug Fixes

* incorrect current qty in stock reco (backport [#51152](https://github.com/frappe/erpnext/issues/51152)) ([#51158](https://github.com/frappe/erpnext/issues/51158)) ([552c5b5](552c5b5911))
2025-12-17 09:34:54 +00:00
rohitwaghchaure
cbd0a76645 Merge pull request #51161 from frappe/mergify/bp/version-15/pr-51158
fix: incorrect current qty in stock reco (backport #51152) (backport #51158)
2025-12-17 15:03:25 +05:30
sudarshan-g
5b1795b0a5 fix: show company currency in asset depreciation schedule
(cherry picked from commit e32f898dd7)
2025-12-17 09:04:31 +00:00
mergify[bot]
552c5b5911 fix: incorrect current qty in stock reco (backport #51152) (#51158)
* fix: incorrect current qty in stock reco (#51152)

(cherry picked from commit dec474ef3a)

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 89d6a8f02e)
2025-12-17 08:17:20 +00:00
mergify[bot]
89d6a8f02e fix: incorrect current qty in stock reco (backport #51152) (#51158)
* fix: incorrect current qty in stock reco (#51152)

(cherry picked from commit dec474ef3a)

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-12-17 13:46:31 +05:30
Diptanil Saha
069262dd4d Merge pull request #51148 from frappe/mergify/bp/version-15-hotfix/pr-48942 2025-12-17 11:55:23 +05:30
Yash Chaubey
048865811c perf: optimize company monthly sales query using date range (#48942)
* perf: optimize company monthly sales query using date range instead of DATE_FORMAT

* perf: optimize company monthly sales query using date range

(cherry picked from commit 4ede97ae2b)
2025-12-17 06:07:27 +00:00
mergify[bot]
2d42904bfb fix: use serial and batch bundle to fetch incoming rate (backport #51119) (#51146)
Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: use serial and batch bundle to fetch incoming rate (#51119)
2025-12-16 18:24:39 +01:00
Sudharsanan11
0b6b73b500 fix(buying): add disabled filter for supplier
(cherry picked from commit 6cc2290f6e)

# Conflicts:
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
2025-12-16 12:51:40 +00:00
Smit Vora
0452b22aa6 fix: use original logic for v15 - inverted wrt v16 2025-12-15 17:20:57 +05:30
Smit Vora
edcf24afa9 chore: resolve conflicts 2025-12-15 14:59:06 +05:30
Smit Vora
e403dfe73a test: add test for projected quantity cascading across multiple sales orders
(cherry picked from commit 92fdec9b92)
2025-12-15 09:08:47 +00:00
Smit Vora
dffd5d9cdd fix: cascade projected quantity across multiple items in material requests
(cherry picked from commit d344be32a0)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.py
2025-12-15 09:08:47 +00:00
128 changed files with 9493 additions and 918 deletions

View File

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

View File

@@ -93,6 +93,7 @@
"receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
@@ -306,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"
@@ -657,6 +658,12 @@
"fieldname": "show_party_balance",
"fieldtype": "Check",
"label": "Show Party Balance"
},
{
"default": "30, 60, 90, 120",
"fieldname": "default_ageing_range",
"fieldtype": "Data",
"label": "Default Ageing Range"
}
],
"icon": "icon-cog",
@@ -664,7 +671,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-11-06 17:48:07.682837",
"modified": "2025-12-26 19:46:55.093717",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -694,4 +701,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -41,6 +41,7 @@ class AccountsSettings(Document):
check_supplier_invoice_uniqueness: DF.Check
create_pr_in_draft_status: DF.Check
credit_controller: DF.Link | None
default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
enable_common_party_accounting: DF.Check

View File

@@ -3,9 +3,6 @@
frappe.provide("erpnext.integrations");
frappe.ui.form.on("Bank", {
onload: function (frm) {
add_fields_to_mapping_table(frm);
},
refresh: function (frm) {
add_fields_to_mapping_table(frm);
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
});
});
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
"bank_transaction_field",
"options",
options
);
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
if (grid) {
grid.update_docfield_property("bank_transaction_field", "options", options);
}
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
)
);
console.log(error);
console.error(error);
}
plaid_success(token, response) {

View File

@@ -304,6 +304,7 @@ def create_payment_entry_bts(
project=None,
cost_center=None,
allow_edit=None,
company_bank_account=None,
):
# Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values(
@@ -345,6 +346,9 @@ def create_payment_entry_bts(
pe.project = project
pe.cost_center = cost_center
if company_bank_account:
pe.bank_account = company_bank_account
pe.validate()
if allow_edit:

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

@@ -20,6 +20,23 @@ frappe.ui.form.on("Journal Entry", {
"Unreconcile Payment Entries",
"Bank Transaction",
];
frm.trigger("set_queries");
},
set_queries(frm) {
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
let filters = {
company: doc.company,
};
if (row.party_type == "Customer") {
filters.customer = row.party;
}
return {
query: "erpnext.controllers.queries.get_project_name",
filters,
};
});
},
refresh: function (frm) {

View File

@@ -6,6 +6,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
@@ -171,16 +172,17 @@ class JournalEntry(AccountsController):
validate_docs_for_deferred_accounting([self.name], [])
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("submit", timeout=4600)
if len(self.accounts) > 100 and not self.meta.queue_in_background:
queue_submission(self, "_submit")
else:
return self._submit()
def before_cancel(self):
self.has_asset_adjustment_entry()
def cancel(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("cancel", timeout=4600)
queue_submission(self, "_cancel")
else:
return self._cancel()
@@ -448,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

@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
);
frm.refresh_fields();
const party_currency =
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
var reference_grid = frm.fields_dict["references"].grid;
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
reference_grid.update_docfield_property(fieldname, "options", party_currency);
});
reference_grid.refresh();
},
show_general_ledger: function (frm) {
@@ -435,6 +445,7 @@ frappe.ui.form.on("Payment Entry", {
"paid_to",
"references",
"total_allocated_amount",
"party_name",
],
function (i, field) {
frm.set_value(field, null);
@@ -1118,7 +1129,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount,
paid_amount: flt(paid_amount),
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
@@ -1520,18 +1531,14 @@ frappe.ui.form.on("Payment Entry", {
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
);
d.row_id = "";
} else if (
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
d.row_id
) {
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
if (d.idx == 1) {
msg = __(
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
);
d.charge_type = "";
} else if (!d.row_id) {
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
d.row_id = "";
d.row_id = d.idx - 1;
} else if (d.row_id && d.row_id >= d.idx) {
msg = __(
"Cannot refer row number greater than or equal to current row number for this Charge type"

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

@@ -132,6 +132,12 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "due_date",

View File

@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
amount_in_account_currency: DF.Currency
company: DF.Link | None
cost_center: DF.Link | None
project: DF.Link | None
delinked: DF.Check
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -133,7 +134,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 +146,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):
@@ -714,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):

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

@@ -13,9 +13,9 @@ frappe.ui.form.on("Period Closing Voucher", {
return {
filters: [
["Account", "company", "=", frm.doc.company],
["Account", "is_group", "=", "0"],
["Account", "is_group", "=", 0],
["Account", "freeze_account", "=", "No"],
["Account", "root_type", "in", "Liability, Equity"],
["Account", "root_type", "in", ["Liability", "Equity"]],
],
};
});

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

@@ -115,6 +115,10 @@ class RepostAccountingLedger(Document):
def generate_preview(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
if not self.vouchers:
frappe.msgprint(_("Add vouchers to generate preview."))
return
gl_columns = []
gl_data = []
@@ -142,6 +146,7 @@ class RepostAccountingLedger(Document):
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
)
frappe.msgprint(_("Repost has started in the background"))
else:
@@ -210,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
@@ -286,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

@@ -117,12 +117,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
return item.delivery_note ? true : false;
});
if (!from_delivery_note && !is_delivered_by_supplier) {
cur_frm.add_custom_button(
__("Delivery"),
cur_frm.cscript["Make Delivery Note"],
__("Create")
if (!is_delivered_by_supplier) {
const should_create_delivery_note = doc.items.some(
(item) =>
item.qty - item.delivered_qty > 0 &&
!item.dn_detail &&
!item.delivered_by_supplier
);
if (should_create_delivery_note) {
this.frm.add_custom_button(
__("Delivery Note"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);
}
}
}

View File

@@ -362,21 +362,34 @@ class SalesInvoice(SellingController):
validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
asset = frappe.get_doc("Asset", d.asset)
if self.doctype == "Sales Invoice" and self.docstatus == 1:
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
if self.doctype != "Sales Invoice":
return
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
asset.status == "Sold" and not self.is_return
):
frappe.throw(
_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(
d.idx, d.asset, asset.status
for d in self.get("items"):
if d.is_fixed_asset:
if d.asset:
if not self.is_return:
asset_status = frappe.db.get_value("Asset", d.asset, "status")
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset_status in ("Scrapped", "Cancelled", "Capitalized"):
frappe.throw(
_("Row #{0}: Asset {1} cannot be sold, it is already {2}").format(
d.idx, d.asset, asset_status
)
)
elif asset_status == "Sold" and not self.is_return:
frappe.throw(_("Row #{0}: Asset {1} is already sold").format(d.idx, d.asset))
elif not self.return_against:
frappe.throw(
_("Row #{0}: Return Against is required for returning asset").format(d.idx)
)
else:
frappe.throw(
_("Row #{0}: You must select an Asset for Item {1}.").format(d.idx, d.item_code),
title=_("Missing Asset"),
)
def validate_item_cost_centers(self):
for item in self.items:
@@ -465,6 +478,8 @@ class SalesInvoice(SellingController):
self.update_stock_reservation_entries()
self.update_stock_ledger()
self.process_asset_depreciation()
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -561,6 +576,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.process_asset_depreciation()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
@@ -1182,6 +1199,91 @@ class SalesInvoice(SellingController):
):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def process_asset_depreciation(self):
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:
self.restore_asset()
self.update_asset()
def depreciate_asset_on_sale(self):
"""
Depreciate asset on sale or cancellation of return sales invoice
"""
disposal_date = self.get_disposal_date()
for d in self.get("items"):
if d.asset:
asset = frappe.get_doc("Asset", d.asset)
if asset.calculate_depreciation and asset.status != "Fully Depreciated":
depreciate_asset(asset, disposal_date, self.get_note_for_asset_sale(asset))
def get_note_for_asset_sale(self, asset):
return _("This schedule was created when Asset {0} was {1} through Sales Invoice {2}.").format(
get_link_to_form(asset.doctype, asset.name),
_("returned") if self.is_return else _("sold"),
get_link_to_form(self.doctype, self.get("name")),
)
def restore_asset(self):
"""
Restore asset on return or cancellation of original sales invoice
"""
for d in self.get("items"):
if d.asset:
asset = frappe.get_cached_doc("Asset", d.asset)
if asset.calculate_depreciation:
posting_date = self.get_disposal_date()
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
note = self.get_note_for_asset_return(asset)
reset_depreciation_schedule(asset, self.posting_date, note)
def get_note_for_asset_return(self, asset):
asset_link = get_link_to_form(asset.doctype, asset.name)
invoice_link = get_link_to_form(self.doctype, self.get("name"))
if self.is_return:
return _(
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
).format(asset_link, invoice_link)
else:
return _(
"This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation."
).format(asset_link, invoice_link)
def update_asset(self):
"""
Update asset status, disposal date and asset activity on sale or return sales invoice
"""
def _update_asset(asset, disposal_date, note, asset_status=None):
frappe.db.set_value("Asset", d.asset, "disposal_date", disposal_date)
add_asset_activity(asset.name, note)
asset.set_status(asset_status)
disposal_date = self.get_disposal_date()
for d in self.get("items"):
if d.asset:
asset = frappe.get_cached_doc("Asset", d.asset)
if (self.is_return and self.docstatus == 1) or (not self.is_return and self.docstatus == 2):
note = _("Asset returned") if self.is_return else _("Asset sold")
asset_status, disposal_date = None, None
else:
note = _("Asset sold") if not self.is_return else _("Return invoice of asset cancelled")
asset_status = "Sold"
_update_asset(asset, disposal_date, note, asset_status)
def get_disposal_date(self):
if self.is_return:
disposal_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
else:
disposal_date = self.posting_date
return disposal_date
def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
@@ -1358,68 +1460,8 @@ class SalesInvoice(SellingController):
if self.is_internal_transfer():
continue
if item.is_fixed_asset:
asset = self.get_asset(item)
if (self.docstatus == 2 and not self.is_return) or (
self.docstatus == 1 and self.is_return
):
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", None)
add_asset_activity(asset.name, _("Asset returned"))
asset_status = asset.get_status()
if asset.calculate_depreciation and not asset_status == "Fully Depreciated":
posting_date = (
frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
if self.is_return
else self.posting_date
)
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
notes = _(
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
reset_depreciation_schedule(asset, self.posting_date, notes)
asset.reload()
else:
if asset.calculate_depreciation:
if not asset.status == "Fully Depreciated":
notes = _(
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", self.posting_date)
add_asset_activity(asset.name, _("Asset sold"))
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
self.set_asset_status(asset)
if item.is_fixed_asset and item.asset:
self.get_gl_entries_for_fixed_asset(item, gl_entries)
else:
income_account = (
@@ -1455,17 +1497,31 @@ class SalesInvoice(SellingController):
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super().get_gl_entries()
def get_asset(self, item):
if item.get("asset"):
asset = frappe.get_doc("Asset", item.asset)
def get_gl_entries_for_fixed_asset(self, item, gl_entries):
asset = frappe.get_cached_doc("Asset", item.asset)
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
else:
frappe.throw(
_("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
title=_("Missing Asset"),
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
self.check_finance_books(item, asset)
return asset
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
@property
def enable_discount_accounting(self):
@@ -2155,7 +2211,9 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1,
"condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.dn_detail
and doc.qty - doc.delivered_qty > 0,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {

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(
@@ -4721,6 +4775,66 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(q[0][0], 1)
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
item_code = "_Test Item for Expiry Batch Zero Valuation"
make_item_for_si(
item_code,
{
"is_stock_item": 1,
"has_batch_no": 1,
"has_expiry_date": 1,
"shelf_life_in_days": 2,
"create_new_batch": 1,
"batch_number_series": "TBATCH-EBZV.####",
},
)
se = make_stock_entry(
item_code=item_code,
qty=10,
target="_Test Warehouse - _TC",
rate=100,
)
# fetch batch no from bundle
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
si = create_sales_invoice(
posting_date=add_days(nowdate(), 3),
item=item_code,
qty=-10,
rate=100,
is_return=1,
update_stock=1,
use_serial_batch_fields=1,
do_not_save=1,
do_not_submit=1,
)
si.items[0].batch_no = batch_no
si.save()
si.submit()
si.reload()
# check zero incoming rate in voucher
self.assertEqual(si.items[0].incoming_rate, 0.0)
# chekc zero incoming rate in stock ledger
stock_ledger_entry = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
"item_code": item_code,
"warehouse": "_Test Warehouse - _TC",
},
["incoming_rate", "valuation_rate"],
as_dict=True,
)
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

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

@@ -1056,3 +1056,21 @@ def add_party_account(party_type, party, company, account):
def render_address(address, check_permissions=True):
return frappe.call(_render_address, address, check_permissions=check_permissions)
def validate_party_currency_before_merging(party_type, old_party, new_party):
for company in frappe.get_all("Company"):
old_party_currency = get_party_gle_currency(party_type, old_party, company.name)
new_party_currency = get_party_gle_currency(party_type, new_party, company.name)
if old_party_currency and new_party_currency and old_party_currency != new_party_currency:
frappe.throw(
_(
"Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'."
).format(
party_type,
old_party,
new_party,
company.name,
)
)

View File

@@ -165,6 +165,10 @@ frappe.query_reports["Accounts Payable"] = {
var filters = report.get_values();
frappe.set_route("query-report", "Accounts Payable Summary", { company: filters.company });
});
if (frappe.boot.sysdefaults.default_ageing_range) {
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
}
},
};

View File

@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{
fieldname: "show_gl_balance",
label: __("Show GL Balance"),
fieldtype: "Check",
},
],
onload: function (report) {
@@ -109,6 +114,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
var filters = report.get_values();
frappe.set_route("query-report", "Accounts Payable", { company: filters.company });
});
if (frappe.boot.sysdefaults.default_ageing_range) {
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
}
},
};

View File

@@ -192,6 +192,10 @@ frappe.query_reports["Accounts Receivable"] = {
var filters = report.get_values();
frappe.set_route("query-report", "Accounts Receivable Summary", { company: filters.company });
});
if (frappe.boot.sysdefaults.default_ageing_range) {
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
}
},
};

View File

@@ -137,6 +137,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
var filters = report.get_values();
frappe.set_route("query-report", "Accounts Receivable", { company: filters.company });
});
if (frappe.boot.sysdefaults.default_ageing_range) {
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
}
},
};

View File

@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
for party, party_dict in self.party_total.items():
if flt(party_dict.outstanding, self.currency_precision) == 0:
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
def get_gl_balance(report_date, company):
def get_gl_balance(report_date, company, account_type):
if account_type == "Payable":
balance_calc_fields = ["party", "SUM(credit - debit) AS balance"]
else:
balance_calc_fields = ["party", "SUM(debit - credit) AS balance"]
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
fields=balance_calc_fields,
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,

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

@@ -81,5 +81,11 @@ frappe.query_reports["Trial Balance for Party"] = {
label: __("Show zero values"),
fieldtype: "Check",
},
{
fieldname: "exclude_zero_balance_parties",
label: __("Exclude Zero Balance Parties"),
fieldtype: "Check",
default: 1,
},
],
};

View File

@@ -75,20 +75,20 @@ def get_data(filters, show_party_name):
closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit)
row.update({"closing_debit": closing_debit, "closing_credit": closing_credit})
# totals
for col in total_row:
total_row[col] += row.get(col)
row.update({"currency": company_currency})
has_value = False
if opening_debit or opening_credit or debit or credit or closing_debit or closing_credit:
has_value = True
# Exclude zero balance parties if filter is set
if filters.get("exclude_zero_balance_parties") and not closing_debit and not closing_credit:
continue
if cint(filters.show_zero_values) or has_value:
data.append(row)
# Add total row
# totals
for col in total_row:
total_row[col] += row.get(col)
total_row.update({"party": "'" + _("Totals") + "'", "currency": company_currency})
data.append(total_row)

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
@@ -1867,6 +1868,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
account=gle.account,
party_type=gle.party_type,
party=gle.party,
project=gle.project,
cost_center=gle.cost_center,
finance_book=gle.finance_book,
due_date=gle.due_date,

View File

@@ -80,6 +80,12 @@ frappe.ui.form.on("Asset", {
}
},
before_submit: function (frm) {
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) {
frappe.throw(__("Please capitalize this asset before submitting."));
}
},
refresh: function (frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -200,9 +206,10 @@ frappe.ui.form.on("Asset", {
asset: frm.doc.name,
},
callback: function (r) {
frm.has_active_capitalization = r.message;
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text(__("Capitalize this asset to confirm"));
$(".form-message").text(__("Capitalize this asset before submitting."));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");
@@ -228,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) {
@@ -274,8 +319,14 @@ frappe.ui.form.on("Asset", {
const row = [
sch["idx"],
frappe.format(sch["schedule_date"], { fieldtype: "Date" }),
frappe.format(sch["depreciation_amount"], { fieldtype: "Currency" }),
frappe.format(sch["accumulated_depreciation_amount"], { fieldtype: "Currency" }),
frappe.format(sch["depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
frappe.format(sch["accumulated_depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
sch["journal_entry"] || "",
];
@@ -468,11 +519,9 @@ frappe.ui.form.on("Asset", {
is_composite_asset: function (frm) {
if (frm.doc.is_composite_asset) {
frm.set_value("gross_purchase_amount", 0);
frm.set_df_property("gross_purchase_amount", "read_only", 1);
} else {
frm.set_df_property("gross_purchase_amount", "read_only", 0);
}
frm.trigger("toggle_reference_doc");
},
@@ -536,7 +585,6 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
$(".primary-action").prop("hidden", false);
},
});
},

View File

@@ -229,7 +229,8 @@
"fieldtype": "Currency",
"label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency"
"options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.is_composite_asset"
},
{
"fieldname": "available_for_use_date",
@@ -596,7 +597,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-11-17 18:01:51.417942",
"modified": "2025-12-23 16:01:10.195932",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -69,7 +69,6 @@ class Asset(AccountsController):
default_finance_book: DF.Link | None
department: DF.Link | None
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
depreciation_completed: DF.Check
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"]
disposal_date: DF.Date | None
finance_books: DF.Table[AssetFinanceBook]
@@ -159,6 +158,10 @@ class Asset(AccountsController):
self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
self.status = self.get_status()
def before_submit(self):
if self.is_composite_asset and not has_active_capitalization(self.name):
frappe.throw(_("Please capitalize this asset before submitting."))
def on_submit(self):
self.validate_in_use_date()
self.make_asset_movement()
@@ -477,6 +480,7 @@ class Asset(AccountsController):
def set_depreciation_rate(self):
for d in self.get("finance_books"):
self.validate_asset_finance_books(d)
d.rate_of_depreciation = flt(
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
@@ -485,6 +489,10 @@ class Asset(AccountsController):
row.expected_value_after_useful_life = flt(
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
)
if flt(row.expected_value_after_useful_life) < 0:
frappe.throw(_("Row {0}: Expected Value After Useful Life cannot be negative").format(row.idx))
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
@@ -500,50 +508,71 @@ class Asset(AccountsController):
title=_("Invalid Schedule"),
)
row.depreciation_start_date = get_last_day(self.available_for_use_date)
self.validate_depreciation_start_date(row)
self.validate_total_number_of_depreciations_and_frequency(row)
if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0
self.opening_number_of_booked_depreciations = 0
else:
depreciable_amount = flt(
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
self.precision("gross_purchase_amount"),
)
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
self.validate_opening_depreciation_values(row)
def validate_depreciation_start_date(self, row):
if row.depreciation_start_date:
if getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
depreciable_amount
_("Row #{0}: Next Depreciation Date cannot be before Purchase Date").format(row.idx)
)
if getdate(row.depreciation_start_date) < getdate(self.available_for_use_date):
frappe.throw(
_("Row #{0}: Next Depreciation Date cannot be before Available-for-use Date").format(
row.idx
)
)
if self.opening_accumulated_depreciation:
if not self.opening_number_of_booked_depreciations:
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
else:
self.opening_number_of_booked_depreciations = 0
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
frappe.throw(
_(
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
).format(row.idx),
title=_("Invalid Schedule"),
)
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
else:
frappe.throw(
_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format(
row.idx
_("Row #{0}: Depreciation Start Date is required").format(row.idx),
title=_("Invalid Schedule"),
)
def validate_total_number_of_depreciations_and_frequency(self, row):
if row.total_number_of_depreciations <= 0:
frappe.throw(
_("Row #{0}: Total Number of Depreciations must be greater than zero").format(row.idx)
)
if row.frequency_of_depreciation <= 0:
frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx))
def validate_opening_depreciation_values(self, row):
row.expected_value_after_useful_life = flt(
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
)
depreciable_amount = flt(
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
self.precision("gross_purchase_amount"),
)
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
frappe.throw(
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
depreciable_amount
)
)
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(
self.available_for_use_date
):
if self.opening_accumulated_depreciation:
if not self.opening_number_of_booked_depreciations:
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
else:
self.opening_number_of_booked_depreciations = 0
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
frappe.throw(
_(
"Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date"
).format(row.idx)
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
).format(row.idx),
title=_("Invalid Schedule"),
)
def set_total_booked_depreciations(self):
@@ -640,7 +669,10 @@ class Asset(AccountsController):
def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0:
status = "Draft"
if self.is_composite_asset:
status = "Work In Progress"
else:
status = "Draft"
elif self.docstatus == 1:
status = "Submitted"

View File

@@ -197,6 +197,13 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
}
serial_and_batch_bundle(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Stock Item") {
this.get_warehouse_details(row);
}
}
asset(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Asset Item") {
@@ -410,6 +417,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
voucher_type: me.frm.doc.doctype,
voucher_no: me.frm.doc.name,
allow_zero_valuation: 1,
serial_and_batch_bundle: item.serial_and_batch_bundle,
},
},
callback: function (r) {

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
@@ -363,6 +364,7 @@ class AssetCapitalization(StockController):
"voucher_no": self.name,
"company": self.company,
"allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")),
"serial_and_batch_bundle": item.serial_and_batch_bundle,
}
)
@@ -609,14 +611,21 @@ class AssetCapitalization(StockController):
asset_doc = frappe.get_doc("Asset", self.target_asset)
if self.docstatus == 2:
asset_doc.gross_purchase_amount -= total_target_asset_value
asset_doc.purchase_amount -= total_target_asset_value
gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
else:
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
asset_doc.set_status("Work In Progress")
asset_doc.flags.ignore_validate = True
asset_doc.save()
gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
asset_doc.db_set(
{
"gross_purchase_amount": gross_purchase_amount,
"purchase_amount": purchase_amount,
"total_asset_cost": total_asset_cost,
}
)
frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
@@ -763,6 +772,7 @@ def get_consumed_stock_item_details(args):
"company": args.company,
"serial_no": args.serial_no,
"batch_no": args.batch_no,
"serial_and_batch_bundle": args.serial_and_batch_bundle,
}
)
out.update(get_warehouse_details(incoming_rate_args))

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2017-10-23 11:38:54.004355",
"doctype": "DocType",
@@ -250,7 +251,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-17 18:35:54.575265",
"modified": "2026-01-06 15:48:13.862505",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -264,6 +265,7 @@
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,
@@ -279,6 +281,7 @@
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,
@@ -295,4 +298,4 @@
"title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
}

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"),
@@ -555,7 +565,10 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
doctype: "Supplier",
order_by: "name",
fields: ["name"],
filters: [["Supplier", "supplier_group", "=", args.supplier_group]],
filters: [
["Supplier", "supplier_group", "=", args.supplier_group],
["disabled", "=", 0],
],
},
callback: load_suppliers,
});

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

@@ -14,6 +14,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_
from erpnext.accounts.party import (
get_dashboard_info,
validate_party_accounts,
validate_party_currency_before_merging,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -208,6 +209,10 @@ class Supplier(TransactionBase):
delete_contact_and_address("Supplier", self.name)
def before_rename(self, olddn, newdn, merge=False):
if merge:
validate_party_currency_before_merging("Supplier", olddn, newdn)
def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name":
self.db_set("supplier_name", newdn)

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

@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate
class StockOverReturnError(frappe.ValidationError):
@@ -683,6 +683,29 @@ def get_rate_for_return(
else:
select_field = "abs(stock_value_difference / actual_qty)"
item_details = frappe.get_cached_value("Item", item_code, ["has_batch_no", "has_expiry_date"], as_dict=1)
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
"Selling Settings", "set_zero_rate_for_expired_batch"
)
if (
set_zero_rate_for_expired_batch
and item_details.has_batch_no
and item_details.has_expiry_date
and not return_against
and voucher_type in ["Sales Invoice", "Delivery Note"]
):
# set incoming_rate zero explicitly for standalone credit note with expired batch
batch_no = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "batch_no")
if batch_no and is_batch_expired(batch_no, sle.get("posting_date")):
frappe.db.set_value(
voucher_type + " Item",
voucher_detail_no,
"incoming_rate",
0,
)
return 0
rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]:
rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate")
@@ -1152,3 +1175,17 @@ def get_available_serial_nos(serial_nos, warehouse):
def get_payment_data(invoice):
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
return payment
def is_batch_expired(batch_no, posting_date):
"""
To check whether the batch is expired or not based on the posting date.
"""
expiry_date = frappe.db.get_value("Batch", batch_no, "expiry_date")
if not expiry_date:
return
if getdate(posting_date) > getdate(expiry_date):
return True
return False

View File

@@ -8,7 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, nowtime
from erpnext.accounts.party import render_address
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, is_batch_expired
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
@@ -521,16 +521,31 @@ class SellingController(StockController):
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
"Selling Settings", "set_zero_rate_for_expired_batch"
)
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
item_details = frappe.get_cached_value(
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
"Item", d.item_code, ["has_serial_no", "has_batch_no", "has_expiry_date"], as_dict=1
)
if not self.get("return_against") or (
if (
set_zero_rate_for_expired_batch
and item_details.has_batch_no
and item_details.has_expiry_date
and self.get("is_return")
and not self.get("return_against")
and is_batch_expired(d.batch_no, self.get("posting_date"))
):
# set incoming rate as zero for stand-lone credit note with expired batch
d.incoming_rate = 0
elif not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average"
and self.get("is_return")
and not item_details.has_serial_no
@@ -1004,10 +1019,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

@@ -465,7 +465,7 @@ class StockController(AccountsController):
if is_rejected:
serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward"
qty = row.get("rejected_qty")
qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
warehouse = row.get("rejected_warehouse")
if (

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

@@ -553,8 +553,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
do_not_explode: d.do_not_explode,
},
callback: function (r) {
d = locals[cdt][cdn];
$.extend(d, r.message);
refresh_field("items");
refresh_field("scrap_items");

View File

@@ -369,6 +369,15 @@ class ProductionPlan(Document):
pi = frappe.qb.DocType("Packed Item")
pending_qty = (
frappe.qb.terms.Case()
.when(
(so_item.work_order_qty > so_item.delivered_qty),
(((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty),
)
.else_(((so_item.qty - so_item.delivered_qty) * pi.qty) / so_item.qty)
)
packed_items_query = (
frappe.qb.from_(so_item)
.from_(pi)
@@ -376,7 +385,7 @@ class ProductionPlan(Document):
pi.parent,
pi.item_code,
pi.warehouse.as_("warehouse"),
(((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"),
pending_qty.as_("pending_qty"),
pi.parent_item,
pi.description,
so_item.name,
@@ -387,7 +396,16 @@ class ProductionPlan(Document):
& (so_item.docstatus == 1)
& (pi.parent_item == so_item.item_code)
& (so_item.parent.isin(so_list))
& (so_item.qty > so_item.work_order_qty)
& (
(
(so_item.work_order_qty > so_item.delivered_qty)
& (so_item.qty > so_item.work_order_qty)
)
| (
(so_item.work_order_qty <= so_item.delivered_qty)
& (so_item.qty > so_item.delivered_qty)
)
)
& (
ExistsCriterion(
frappe.qb.from_(bom)
@@ -1303,14 +1321,21 @@ def get_material_request_items(
include_safety_stock,
warehouse,
bin_dict,
consumed_qty,
):
total_qty = row["qty"]
required_qty = 0
item_code = row.get("item_code")
if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
required_qty = total_qty
elif total_qty > bin_dict.get("projected_qty", 0):
required_qty = total_qty - bin_dict.get("projected_qty", 0)
required_qty = flt(row.get("qty"))
else:
key = (item_code, warehouse)
available_qty = flt(bin_dict.get("projected_qty", 0)) - consumed_qty[key]
if available_qty > 0:
required_qty = max(0, flt(row.get("qty")) - available_qty)
consumed_qty[key] += min(flt(row.get("qty")), available_qty)
else:
required_qty = flt(row.get("qty"))
if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]:
required_qty = row["min_order_qty"]
@@ -1354,7 +1379,7 @@ def get_material_request_items(
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty,
"required_bom_qty": row.get("qty"),
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
or row.get("source_warehouse")
@@ -1648,9 +1673,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details[sales_order][item_code] = details
mr_items = []
consumed_qty = defaultdict(float)
for sales_order in so_item_details:
item_dict = so_item_details[sales_order]
for details in item_dict.values():
warehouse = warehouse or details.get("source_warehouse") or details.get("default_warehouse")
bin_dict = get_bin_details(details, doc.company, warehouse)
bin_dict = bin_dict[0] if bin_dict else {}
@@ -1664,6 +1692,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock,
warehouse,
bin_dict,
consumed_qty,
)
if items:
mr_items.append(items)

View File

@@ -145,6 +145,84 @@ class TestProductionPlan(FrappeTestCase):
sr2.cancel()
pln.cancel()
def test_projected_qty_cascading_across_multiple_sales_orders(self):
rm_item = make_item(
"_Test RM For Cascading",
{"is_stock_item": 1, "valuation_rate": 100},
).name
fg_item_a = make_item(
"_Test FG A For Cascading",
{"is_stock_item": 1, "valuation_rate": 200},
).name
if not frappe.db.exists("BOM", {"item": fg_item_a, "docstatus": 1}):
make_bom(item=fg_item_a, raw_materials=[rm_item], rm_qty=1)
# Stock for RM
sr = create_stock_reconciliation(item_code=rm_item, target="_Test Warehouse - _TC", qty=1, rate=100)
# Sales orders
so1 = make_sales_order(item_code=fg_item_a, qty=1)
so2 = make_sales_order(item_code=fg_item_a, qty=1)
so3 = make_sales_order(item_code=fg_item_a, qty=1)
# Production plan
pln = frappe.get_doc(
{
"doctype": "Production Plan",
"company": "_Test Company",
"posting_date": nowdate(),
"get_items_from": "Sales Order",
"ignore_existing_ordered_qty": 0,
}
)
pln.append(
"sales_orders",
{
"sales_order": so1.name,
"sales_order_date": so1.transaction_date,
"customer": so1.customer,
"grand_total": so1.grand_total,
},
)
pln.append(
"sales_orders",
{
"sales_order": so2.name,
"sales_order_date": so2.transaction_date,
"customer": so2.customer,
"grand_total": so2.grand_total,
},
)
pln.append(
"sales_orders",
{
"sales_order": so3.name,
"sales_order_date": so3.transaction_date,
"customer": so3.customer,
"grand_total": so3.grand_total,
},
)
pln.get_items()
pln.insert()
mr_items = get_items_for_material_requests(pln.as_dict())
quantities = [d["quantity"] for d in mr_items]
rm_qty = sum(quantities)
# Only 2 MR item created - the first SO's requirement is fully covered by stock (v15 behaviour)
self.assertEqual(len(mr_items), 2)
self.assertEqual(rm_qty, 2, "Cascading failed: total MR qty should be 2 (3 needed - 1 in stock)")
self.assertEqual(
quantities,
[1, 1],
"Cascading failed: only second and third SO should need procurement (qty=1) since first SO consumed stock",
)
sr.cancel()
def test_production_plan_with_non_stock_item(self):
"Test if MR Planning table includes Non Stock RM."
pln = create_production_plan(item_code="Test Production Item 1", include_non_stock_items=1)
@@ -678,6 +756,109 @@ class TestProductionPlan(FrappeTestCase):
frappe.db.rollback()
def test_get_sales_order_items_for_product_bundle(self):
"""Testing the Planned Qty for Product Bundle Item"""
from erpnext.manufacturing.doctype.work_order.test_work_order import (
make_stock_entry as create_stock_entry,
)
from erpnext.manufacturing.doctype.work_order.test_work_order import (
make_wo_order_test_record,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
# 1. Create required items
bundle_item = create_item(item_code="Bundle Item", is_stock_item=0)
bom_item = create_item(item_code="BOM Item")
rm_item = create_item(item_code="RM Item")
fg_warehouse = "_Test FG Warehouse - _TC"
# Create warehouse if it doesn't exist
if not frappe.db.exists("Warehouse", fg_warehouse):
create_warehouse(warehouse_name="_Test FG Warehouse")
# 2. Create initial stock for components
make_stock_entry(item_code=bom_item.name, target="_Test FG Warehouse - _TC", qty=15)
make_stock_entry(item_code=rm_item.name, target="Stores - _TC", qty=25)
# 3. Create BOM for manufactured item
bom = make_bom(
item=bom_item.name,
raw_materials=[rm_item.name],
set_as_default_bom=1,
)
# 4. Create Product Bundle (Bundle Item → contains BOM Item)
make_product_bundle(parent=bundle_item.name, items=[bom_item.name])
# 5. Create Sales Order for 50 units of Bundle Item
sales_order = make_sales_order(item_code=bundle_item.name, qty=50, warehouse=fg_warehouse)
# 6. Create Work Order for partial quantity (25 out of 50)
work_order_qty = 25
work_order = make_wo_order_test_record(
production_item=bom_item.name,
bom_no=bom.name,
qty=work_order_qty,
sales_order=sales_order.name,
source_warehouse="Stores - _TC",
fg_warehouse=fg_warehouse,
do_not_save=1,
)
# Link Work Order to correct Sales Order Item row
work_order.sales_order_item = sales_order.items[0].name
work_order.save()
work_order.submit()
# 7. Material transfer from Stores → WIP
transfer_entry = frappe.get_doc(
create_stock_entry(work_order.name, "Material Transfer for Manufacture")
)
for d in transfer_entry.get("items"):
d.s_warehouse = "Stores - _TC"
transfer_entry.insert()
transfer_entry.submit()
# 8. Complete manufacturing (WIP → Finished Goods)
manufacture_entry = frappe.get_doc(create_stock_entry(work_order.name, "Manufacture"))
manufacture_entry.insert()
manufacture_entry.submit()
# 9. Verify work order qty is correctly updated in Sales Order
sales_order.reload()
self.assertEqual(sales_order.items[0].work_order_qty, work_order_qty)
# 10. Create partial Delivery Note (40 out of 50)
dn = make_delivery_note(sales_order.name)
dn.items[0].qty = 40
dn.save()
dn.submit()
# 11. Check delivered quantity updated correctly
sales_order.reload()
self.assertEqual(sales_order.items[0].delivered_qty, 40)
# 12. Create Production Plan from remaining open Sales Order quantity
pln = frappe.new_doc("Production Plan")
pln.company = sales_order.company
pln.get_items_from = "Sales Order"
pln.item_code = bundle_item.name
# Fetch open sales orders
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, sales_order.name)
# Pull items → should plan remaining 10 qty
pln.get_so_items()
"""
Test Case: Production Plan should plan remaining 10 units
(50 ordered - 25 manufactured - 40 delivered = 10 pending)
"""
self.assertEqual(pln.po_items[0].planned_qty, 10)
def test_multiple_work_order_for_production_plan_item(self):
"Test producing Prod Plan (making WO) in parts."

View File

@@ -2467,6 +2467,259 @@ class TestWorkOrder(FrappeTestCase):
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
)
def test_disassembly_with_multiple_manufacture_entries(self):
"""
Test that disassembly does not create duplicate items when manufacturing
is done in multiple batches (multiple manufacture stock entries).
Scenario:
1. Create Work Order for 10 units
2. Transfer raw materials
3. Manufacture in 2 parts (3 units, then 7 units) - creates 2 stock entries
4. Create Disassembly for 4 units
5. Verify no duplicate items in the disassembly stock entry
"""
# Create RM and FG item
raw_item1 = make_item("Test Raw for Multi Batch Disassembly 1", {"is_stock_item": 1}).name
raw_item2 = make_item("Test Raw for Multi Batch Disassembly 2", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Multi Batch Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2)
# Create WO
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
# Ensure enough stock
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
make_stock_entry_test_record(
item_code=raw_item1,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=50,
basic_rate=100,
)
make_stock_entry_test_record(
item_code=raw_item2,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=50,
basic_rate=100,
)
# Transfer for manufacture
se_for_material_transfer = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
)
for item in se_for_material_transfer.items:
item.s_warehouse = wo.wip_warehouse
se_for_material_transfer.save()
se_for_material_transfer.submit()
# First Manufacture Entry - 3 units
se_manufacture1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
se_manufacture1.submit()
# Second Manufacture Entry - 7 units
se_manufacture2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 7))
se_manufacture2.submit()
wo.reload()
self.assertEqual(wo.produced_qty, 10)
# Count manufacture entries
manufacture_entries = frappe.get_all(
"Stock Entry",
filters={
"work_order": wo.name,
"purpose": "Manufacture",
"docstatus": 1,
},
)
self.assertEqual(len(manufacture_entries), 2, "Expected 2 manufacture entries")
# Disassembly for 4 units
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
stock_entry.save()
stock_entry.submit()
item_counts = {}
for item in stock_entry.items:
item_code = item.item_code
item_counts[item_code] = item_counts.get(item_code, 0) + 1
# No duplicates
duplicates = {k: v for k, v in item_counts.items() if v > 1}
self.assertEqual(
len(duplicates),
0,
f"Found duplicate items in disassembly stock entry: {duplicates}",
)
expected_items = 3 # FG item + 2 raw materials
self.assertEqual(
len(stock_entry.items),
expected_items,
f"Expected {expected_items} items, found {len(stock_entry.items)}",
)
# FG item qty
fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertEqual(fg_item_row.qty, disassemble_qty)
# RM quantities
for bom_item in bom.items:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
rm_row = next((i for i in stock_entry.items if i.item_code == bom_item.item_code), None)
self.assertAlmostEqual(
rm_row.qty,
expected_qty,
places=3,
msg=f"Raw material {bom_item.item_code} qty mismatch",
)
def test_disassembly_with_additional_rm_not_in_bom(self):
"""
Test that disassembly correctly handles additional raw materials that were
manually added during manufacturing (not part of the BOM).
Scenario:
1. Create Work Order for 10 units with 2 raw materials in BOM
2. Transfer raw materials for manufacture
3. Manufacture in 2 parts (3 units, then 7 units)
4. In each manufacture entry, manually add an extra consumable item
(not in BOM) in proportion to the manufactured qty
5. Create Disassembly for 4 units
6. Verify that the additional RM is included in disassembly with proportional qty
"""
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
# Create RM and FG item
raw_item1 = make_item("Test BOM Raw 1 for Additional RM Disassembly", {"is_stock_item": 1}).name
raw_item2 = make_item("Test BOM Raw 2 for Additional RM Disassembly", {"is_stock_item": 1}).name
additional_rm = make_item("Test Additional RM for Disassembly", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Additional RM Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2)
# Create WO
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
# Ensure enough stock
for item in [raw_item1, raw_item2, additional_rm]:
make_stock_entry_test_record(
item_code=item,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=100,
basic_rate=100,
)
# Transfer for manufacture
se_for_material_transfer = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
)
for item in se_for_material_transfer.items:
item.s_warehouse = wo.wip_warehouse
se_for_material_transfer.save()
se_for_material_transfer.submit()
# First Manufacture Entry - 3 units
se_manufacture1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
# Additional RM
se_manufacture1.append(
"items",
{
"item_code": additional_rm,
"qty": 3, # 1 per unit
"s_warehouse": wo.wip_warehouse,
"is_finished_item": 0,
},
)
se_manufacture1.save()
se_manufacture1.submit()
# Second Manufacture Entry - 7 units
se_manufacture2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 7))
# AAdditional RM
se_manufacture2.append(
"items",
{
"item_code": additional_rm,
"qty": 7, # 1 per unit
"s_warehouse": wo.wip_warehouse,
"is_finished_item": 0,
},
)
se_manufacture2.save()
se_manufacture2.submit()
wo.reload()
self.assertEqual(wo.produced_qty, 10)
# Disassembly for 4 units
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
stock_entry.save()
stock_entry.submit()
# No duplicate
item_counts = {}
for item in stock_entry.items:
item_code = item.item_code
item_counts[item_code] = item_counts.get(item_code, 0) + 1
duplicates = {k: v for k, v in item_counts.items() if v > 1}
self.assertEqual(
len(duplicates),
0,
f"Found duplicate items in disassembly stock entry: {duplicates}",
)
# Additional RM qty
additional_rm_row = next((i for i in stock_entry.items if i.item_code == additional_rm), None)
self.assertIsNotNone(
additional_rm_row,
f"Additional raw material {additional_rm} not found in disassembly",
)
# intentional full reversal as not part of BOM
# eg: dies or consumables used during manufacturing
expected_additional_rm_qty = 3 + 7
self.assertAlmostEqual(
additional_rm_row.qty,
expected_additional_rm_qty,
places=3,
msg=f"Additional RM qty mismatch: expected {expected_additional_rm_qty}, got {additional_rm_row.qty}",
)
# RM qty
for bom_item in bom.items:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
rm_row = next((i for i in stock_entry.items if i.item_code == bom_item.item_code), None)
self.assertIsNotNone(rm_row, f"BOM raw material {bom_item.item_code} not found")
self.assertAlmostEqual(
rm_row.qty,
expected_qty,
places=3,
msg=f"BOM raw material {bom_item.item_code} qty mismatch",
)
# FG qty
fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertEqual(fg_item_row.qty, disassemble_qty)
expected_items = 4
self.assertEqual(
len(stock_entry.items),
expected_items,
f"Expected {expected_items} items, found {len(stock_entry.items)}",
)
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
@@ -2933,6 +3186,53 @@ class TestWorkOrder(FrappeTestCase):
allow_overproduction("overproduction_percentage_for_work_order", 0)
def test_reserved_qty_for_pp_with_extra_material_transfer(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
rm_item_code = make_item(
"_Test Reserved Qty PP Item",
{
"is_stock_item": 1,
},
).name
fg_item_code = make_item(
"_Test Reserved Qty PP FG Item",
{
"is_stock_item": 1,
},
).name
make_stock_entry_test_record(
item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100
)
make_bom(
item=fg_item_code,
raw_materials=[rm_item_code],
)
wo_order = make_wo_order_test_record(
item=fg_item_code,
qty=1,
source_warehouse="_Test Warehouse - _TC",
skip_transfer=0,
target_warehouse="_Test Warehouse - _TC",
)
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1)
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
s.items[0].qty += 2 # extra material transfer
s.submit()
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -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,10 @@ class WorkOrder(Document):
self.validate_cancel()
self.db_set("status", "Cancelled")
self.on_close_or_cancel()
self.delete_job_card()
def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan}
):
@@ -522,7 +527,6 @@ class WorkOrder(Document):
else:
self.update_work_order_qty_in_so()
self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
@@ -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
@@ -1782,6 +1786,9 @@ def get_reserved_qty_for_production(
qty_field = wo_item.required_qty
else:
qty_field = Case()
qty_field = qty_field.when(
((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0
)
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)

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

@@ -2,6 +2,15 @@ import frappe
def execute():
try:
from erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter import execute
execute()
except ImportError:
update_frankfurter_app_parameter_and_result()
def update_frankfurter_app_parameter_and_result():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return

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

@@ -603,7 +603,7 @@ def send_project_update_email_to_users(project):
"sent": 0,
"date": today(),
"time": nowtime(),
"naming_series": "UPDATE-.project.-.YY.MM.DD.-",
"naming_series": "UPDATE-.project.-.YY.MM.DD.-.####",
}
).insert()

View File

@@ -17,6 +17,15 @@ class TestTimesheet(unittest.TestCase):
def setUp(self):
frappe.db.delete("Timesheet")
def test_timesheet_base_amount(self):
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
self.assertEqual(timesheet.time_logs[0].base_billing_rate, 50)
self.assertEqual(timesheet.time_logs[0].base_costing_rate, 20)
self.assertEqual(timesheet.time_logs[0].base_billing_amount, 100)
self.assertEqual(timesheet.time_logs[0].base_costing_amount, 40)
def test_timesheet_billing_amount(self):
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
@@ -236,4 +245,5 @@ def make_timesheet(
def update_activity_type(activity_type):
activity_type = frappe.get_doc("Activity Type", activity_type)
activity_type.billing_rate = 50.0
activity_type.costing_rate = 20.0
activity_type.save(ignore_permissions=True)

View File

@@ -296,6 +296,20 @@ class Timesheet(Document):
data.billing_amount = data.billing_rate * hours
data.costing_amount = data.costing_rate * costing_hours
exchange_rate = flt(self.get("exchange_rate")) or 1.0
data.base_billing_rate = flt(
data.billing_rate * exchange_rate, data.precision("base_billing_rate")
)
data.base_costing_rate = flt(
data.costing_rate * exchange_rate, data.precision("base_costing_rate")
)
data.base_billing_amount = flt(
data.billing_amount * exchange_rate, data.precision("base_billing_amount")
)
data.base_costing_amount = flt(
data.costing_amount * exchange_rate, data.precision("base_costing_amount")
)
def update_time_rates(self, ts_detail):
if not ts_detail.is_billable:
ts_detail.billing_rate = 0.0

View File

@@ -361,6 +361,21 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
mandatory_depends_on:
"eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
},
{
fieldname: "bank_account",
fieldtype: "Link",
label: "Company Bank Account",
options: "Bank Account",
depends_on: "eval:doc.party",
get_query: function () {
return {
filters: {
is_company_account: 1,
company: this.company,
},
};
},
},
{
fieldname: "project",
fieldtype: "Link",
@@ -511,6 +526,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
mode_of_payment: values.mode_of_payment,
project: values.project,
cost_center: values.cost_center,
company_bank_account: values?.bank_account || this?.bank_account,
},
callback: (response) => {
const alert_string = __("Bank Transaction {0} added as Payment Entry", [
@@ -582,6 +598,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
project: values.project,
cost_center: values.cost_center,
allow_edit: true,
company_bank_account: values?.bank_account || this?.bank_account,
},
callback: (r) => {
const doc = frappe.model.sync(r.message);

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);
}
@@ -2721,10 +2726,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
set_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse);
}
set_target_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
this.autofill_warehouse(
this.frm.doc.packed_items,
"target_warehouse",
this.frm.doc.set_target_warehouse
);
}
set_from_warehouse() {

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

@@ -488,7 +488,30 @@ erpnext.sales_common = {
}
}
project() {
project(doc, cdt, cdn) {
if (!cdt || !cdn) {
if (this.frm.doc.project) {
$.each(this.frm.doc["items"] || [], function (i, item) {
if (!item.project) {
frappe.model.set_value(item.doctype, item.name, "project", doc.project);
}
});
}
} else {
const item = frappe.get_doc(cdt, cdn);
if (item.project) {
$.each(this.frm.doc["items"] || [], function (i, other_item) {
if (!other_item.project) {
frappe.model.set_value(
other_item.doctype,
other_item.name,
"project",
item.project
);
}
});
}
}
let me = this;
if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {
if (this.frm.doc.project) {

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -181,6 +181,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Customer Group",
"link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"oldfieldname": "customer_group",
"oldfieldtype": "Link",
"options": "Customer Group",
@@ -610,7 +611,7 @@
"link_fieldname": "party"
}
],
"modified": "2025-11-25 09:35:56.772949",
"modified": "2026-01-21 17:23:42.151114",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -18,7 +18,11 @@ from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.deprecations import deprecated
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
from erpnext.accounts.party import (
get_dashboard_info,
validate_party_accounts,
validate_party_currency_before_merging,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -110,6 +114,7 @@ class Customer(TransactionBase):
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def get_customer_name(self):
self.customer_name = self.customer_name.strip()
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
count = frappe.db.sql(
"""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer
@@ -367,6 +372,10 @@ class Customer(TransactionBase):
if self.lead_name:
frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name)
def before_rename(self, olddn, newdn, merge=False):
if merge:
validate_party_currency_before_merging("Customer", olddn, newdn)
def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
self.db_set("customer_name", newdn)

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

@@ -35,7 +35,8 @@
"hide_tax_id",
"enable_discount_accounting",
"allow_zero_qty_in_quotation",
"allow_zero_qty_in_sales_order"
"allow_zero_qty_in_sales_order",
"set_zero_rate_for_expired_batch"
],
"fields": [
{
@@ -51,6 +52,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Customer Group",
"link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"options": "Customer Group"
},
{
@@ -223,6 +225,13 @@
"fieldname": "fallback_to_default_price_list",
"fieldtype": "Check",
"label": "Use Prices from Default Price List as Fallback"
},
{
"default": "0",
"description": "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item.",
"fieldname": "set_zero_rate_for_expired_batch",
"fieldtype": "Check",
"label": "Set Incoming Rate as Zero for Expired Batch"
}
],
"grid_page_length": 50,
@@ -231,7 +240,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-09-23 21:10:14.826653",
"modified": "2026-01-24 00:04:33.105916",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -41,6 +41,7 @@ class SellingSettings(Document):
role_to_override_stop_action: DF.Link | None
sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"]
selling_price_list: DF.Link | None
set_zero_rate_for_expired_batch: DF.Check
so_required: DF.Literal["No", "Yes"]
territory: DF.Link | None
validate_selling_price: DF.Check

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 cint, formatdate, 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,29 +771,41 @@ def install_country_fixtures(company, country):
def update_company_current_month_sales(company):
current_month_year = formatdate(today(), "MM-yyyy")
"""Update Company's Total Monthly Sales.
results = frappe.db.sql(
f"""
SELECT
SUM(base_grand_total) AS total,
DATE_FORMAT(`posting_date`, '%m-%Y') AS month_year
FROM
`tabSales Invoice`
WHERE
DATE_FORMAT(`posting_date`, '%m-%Y') = '{current_month_year}'
AND docstatus = 1
AND company = {frappe.db.escape(company)}
GROUP BY
month_year
""",
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

@@ -296,8 +296,20 @@ def update_pegged_currencies():
{"source_currency": "SAR", "pegged_against": "USD", "pegged_exchange_rate": 3.75},
]
# Add items on pegged_currency_item if source_currency and pegged_against currency doc exist.
currencies_exist = frappe.db.get_list(
"Currency", {"name": ["in", ["AED", "BHD", "JOD", "OMR", "QAR", "SAR", "USD"]]}, pluck="name"
)
if "USD" not in currencies_exist:
return
for currency in currencies_to_add:
if currency["source_currency"] not in existing_sources:
if (
currency["source_currency"] in currencies_exist
and currency["source_currency"] not in existing_sources
):
doc.append("pegged_currency_item", currency)
doc.save()

View File

@@ -6,14 +6,14 @@
}
},
"Algeria": {
"Algeria VAT 17%": {
"account_name": "VAT 17%",
"tax_rate": 17.00,
"Algeria TVA 19%": {
"account_name": "TVA 19%",
"tax_rate": 19.00,
"default": 1
},
"Algeria VAT 7%": {
"account_name": "VAT 7%",
"tax_rate": 7.00
"Algeria TVA 9%": {
"account_name": "TVA 9%",
"tax_rate": 9.00
}
},

View File

@@ -64,6 +64,9 @@ def boot_session(bootinfo):
bootinfo.party_account_types = frappe._dict(party_account_types)
bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company")
bootinfo.sysdefaults.default_ageing_range = frappe.db.get_single_value(
"Accounts Settings", "default_ageing_range"
)
def update_page_info(bootinfo):

View File

@@ -78,7 +78,6 @@ class DeprecatedBatchNoValuation:
for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
@deprecated
def get_sle_for_batches(self):
@@ -231,7 +230,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
self.total_qty[d.batch_no] += flt(d.batch_qty)
for d in batch_data:
if self.available_qty.get(d.batch_no):
@@ -332,7 +330,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
self.total_qty[d.batch_no] += flt(d.batch_qty)
if not self.last_sle:
return

View File

@@ -159,14 +159,21 @@ class Batch(Document):
@frappe.whitelist()
def recalculate_batch_qty(self):
batches = get_batch_qty(
batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True
batch_no=self.name,
item_code=self.item,
for_stock_levels=True,
consider_negative_batches=True,
ignore_reserved_stock=True,
)
batch_qty = 0.0
if batches:
for row in batches:
batch_qty += row.get("qty")
self.db_set("batch_qty", batch_qty)
if self.batch_qty != batch_qty:
self.db_set("batch_qty", batch_qty)
frappe.msgprint(_("Batch Qty updated to {0}").format(batch_qty), alert=True)
def set_batchwise_valuation(self):
@@ -238,6 +245,7 @@ def get_batch_qty(
for_stock_levels=False,
consider_negative_batches=False,
do_not_check_future_batches=False,
ignore_reserved_stock=False,
):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -267,6 +275,7 @@ def get_batch_qty(
"for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches,
"do_not_check_future_batches": do_not_check_future_batches,
"ignore_reserved_stock": ignore_reserved_stock,
}
)

View File

@@ -324,6 +324,38 @@ class TestBatch(FrappeTestCase):
self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90)
def test_ignore_reserved_qty(self):
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
batch_item_name = "Reserve Batch Item"
batch_id = "Reserve Batch 1"
# Create Batch Item
self.make_batch_item(batch_item_name)
# Create Batch and Material Receipt Entry with qty 90
self.make_new_batch_and_entry(batch_item_name, batch_id, "_Test Warehouse - _TC")
# Enable Stock Reservation
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
# Create Sales Order with qty 50
sales_order = make_sales_order(
item_code=batch_item_name, warehouse="_Test Warehouse - _TC", qty=50, rate=20
)
# Create Pick List for the Sales Order
pl = create_pick_list(sales_order.name)
pl.submit()
# Create Stock Reservation Entries
pl.create_stock_reservation_entries(notify=False)
batch = frappe.get_doc("Batch", batch_id)
# Recalculate Batch Qty
batch.recalculate_batch_qty()
batch.reload()
# Case: Ignore Reserved Qty
self.assertEqual(batch.batch_qty, 90)
def test_total_batch_qty(self):
self.make_batch_item("ITEM-BATCH-3")
existing_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty"))

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

@@ -228,8 +228,25 @@ class Item(Document):
def validate_description(self):
"""Clean HTML description if set"""
if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")):
old_desc = self.description
self.description = clean_html(self.description)
if (
old_desc
and self.description
and "<img src" in old_desc
and "<img src" not in self.description
):
frappe.msgprint(
_(
'Image in the description has been removed. To disable this behavior, uncheck "{0}" in {1}.'
).format(
frappe.get_meta("Stock Settings").get_label("clean_description_html"),
get_link_to_form("Stock Settings"),
),
alert=True,
)
def validate_customer_provided_part(self):
if self.is_customer_provided_item:
if self.is_purchase_item:

View File

@@ -281,7 +281,6 @@
{
"fieldname": "set_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Set Target Warehouse",
"options": "Warehouse"
@@ -368,7 +367,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-07-31 17:19:01.166208",
"modified": "2026-01-21 12:48:40.792323",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

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",
},

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