Compare commits

..

260 Commits

Author SHA1 Message Date
Frappe PR Bot
3a74968ced chore(release): Bumped to Version 15.91.3
## [15.91.3](https://github.com/frappe/erpnext/compare/v15.91.2...v15.91.3) (2025-12-12)

### Bug Fixes

* **accounts:** handle drop ship in company linked address validation ([5d01cad](5d01cad1d5))
2025-12-12 08:57:31 +00:00
rohitwaghchaure
26725f4e53 Merge pull request #51075 from frappe/mergify/bp/version-15/pr-51072
fix(accounts): handle drop ship in company linked address validation (backport #51034) (backport #51072)
2025-12-12 14:26:09 +05:30
Sudharsanan11
b46d93c709 test(accounts): add validation test for dispatch address with drop ship enabled
(cherry picked from commit f6a96e5563)
(cherry picked from commit 2263f9a477)
2025-12-12 08:37:56 +00:00
Sudharsanan11
5d01cad1d5 fix(accounts): handle drop ship in company linked address validation
(cherry picked from commit 2ec119e561)
(cherry picked from commit b340d7d4f4)
2025-12-12 08:37:56 +00:00
Frappe PR Bot
ca21f16db2 chore(release): Bumped to Version 15.91.2
## [15.91.2](https://github.com/frappe/erpnext/compare/v15.91.1...v15.91.2) (2025-12-11)

### Bug Fixes

* putaway rule not applying on serial nos ([23c82d4](23c82d410b))
* Serial/Batches not fetching when creating Material Transfer from Purchase Receipt ([1e0532f](1e0532f387))
2025-12-11 10:48:40 +00:00
rohitwaghchaure
5324000e2e Merge pull request #51043 from frappe/mergify/bp/version-15/pr-51036
fix: put-away rule not applying on serial nos (backport #51035) (backport #51036)
2025-12-11 16:17:10 +05:30
rohitwaghchaure
9d2055c620 Merge pull request #51042 from frappe/mergify/bp/version-15/pr-51029
fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt (backport #51027) (backport #51029)
2025-12-11 16:16:29 +05:30
Rohit Waghchaure
23c82d410b fix: putaway rule not applying on serial nos
(cherry picked from commit 6bb0bdcdca)
(cherry picked from commit df820aece6)
2025-12-11 10:15:31 +00:00
rohitwaghchaure
580e825ec2 chore: fix conflicts
(cherry picked from commit c8565c47a2)
2025-12-11 10:15:29 +00:00
Rohit Waghchaure
1e0532f387 fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt
(cherry picked from commit d16c50486a)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
(cherry picked from commit f3c70a66b5)
2025-12-11 10:15:29 +00:00
Frappe PR Bot
a2b676b340 chore(release): Bumped to Version 15.91.1
## [15.91.1](https://github.com/frappe/erpnext/compare/v15.91.0...v15.91.1) (2025-12-09)

### Bug Fixes

* add return status for delivery note ([ebb6296](ebb62966d3))
* Adjust asset purchase amounts based on docstatus ([a31fb2a](a31fb2ac6c))
* change is_return value in filter from Yes to 1 ([52e26b6](52e26b6da8))
* conflicts ([bd00a48](bd00a484ea))
* conflicts ([1427b4a](1427b4ac3f))
* cost center not reset ([8a3148e](8a3148eee6))
* ensure payment request button only shows for submitted invoices ([b4053ee](b4053ee0d8))
* fg qty uom in manufacture entry ([70d5726](70d57260d6))
* handle duplicate description in item-wise report ([1a278e7](1a278e7ca0))
* include return invoice discount in discount validation ([bf1c606](bf1c606610))
* incorrect condition ([d9e9f35](d9e9f35230))
* inward same serial / batches in disassembly which were used ([cfbd716](cfbd71693b))
* LCV is not changing the valuation of the repacked item ([8b22d9d](8b22d9d95e))
* missing attribute error when restoring asset ([bde209b](bde209b077))
* performance of the reposting ([8d734df](8d734df63b))
* **picklist:** calculate picked qty excluding the delivered qty ([3785ffe](3785ffe5c9))
* quality inspection showing Not Saved ([abe599a](abe599a49d))
* remove comment ([da88196](da88196a89))
* remove set_only_once from is_fixed_asset ([fd6e42e](fd6e42e15e))
* **sales invoice:** 100% additional discount gl issue with discount accounting ([bd6210a](bd6210a212))
* tds for customer and supplier in Journal Entry (backport [#49963](https://github.com/frappe/erpnext/issues/49963)) ([#50985](https://github.com/frappe/erpnext/issues/50985)) ([f2c556a](f2c556a6cc))
* untranslated string in job card ([b2f6d07](b2f6d07c25))
* variant items not fetched while making BOM for Variant Item ([176ce0d](176ce0d4d6))
2025-12-09 17:00:25 +00:00
Diptanil Saha
691db5b877 Merge pull request #50981 from frappe/version-15-hotfix 2025-12-09 22:28:48 +05:30
Diptanil Saha
7bec3d19ac Merge pull request #50977 from ljain112/fix-item-wise-sales-register
fix: handle duplicate description in item-wise report (backport #50979)
2025-12-09 21:47:16 +05:30
Diptanil Saha
3f85aa3aea Merge pull request #50997 from frappe/mergify/bp/version-15-hotfix/pr-50944
fix: include return invoice discount in discount validation (backport #50944)
2025-12-09 21:39:17 +05:30
Diptanil Saha
9ccf4900fe chore: resolve conflict 2025-12-09 20:52:33 +05:30
ravibharathi656
bf1c606610 fix: include return invoice discount in discount validation
(cherry picked from commit fab1ef5d76)

# Conflicts:
#	erpnext/controllers/taxes_and_totals.py
2025-12-09 15:18:08 +00:00
Mihir Kandoi
6c53d31f2d Merge pull request #50994 from frappe/mergify/bp/version-15-hotfix/pr-50912
fix: add return status for delivery note (backport #50912)
2025-12-09 20:07:22 +05:30
Mihir Kandoi
4de1af498b chore: resolve conflicts 2025-12-09 19:51:26 +05:30
Mihir Kandoi
c65409c348 Merge pull request #50993 from frappe/mergify/bp/version-15-hotfix/pr-50910
fix: validate picklist partial reserved qty (backport #50910)
2025-12-09 18:50:37 +05:30
Pugazhendhi Velu
422aec12cb test: add test for return status in delivery note
(cherry picked from commit 445a255a7f)
2025-12-09 13:06:52 +00:00
Pugazhendhi Velu
52e26b6da8 fix: change is_return value in filter from Yes to 1
(cherry picked from commit af212f520d)
2025-12-09 13:06:52 +00:00
Pugazhendhi Velu
ebb62966d3 fix: add return status for delivery note
(cherry picked from commit dec67eecad)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.py
2025-12-09 13:06:51 +00:00
Sudharsanan11
b05e2910d8 test(picklist): add test for reserved qty after partial delivery
(cherry picked from commit 758553b9fc)
2025-12-09 13:04:46 +00:00
Sudharsanan11
3785ffe5c9 fix(picklist): calculate picked qty excluding the delivered qty
(cherry picked from commit f5b75b27d7)
2025-12-09 13:04:45 +00:00
Diptanil Saha
a4ab198042 Merge pull request #50991 from frappe/mergify/bp/version-15-hotfix/pr-50970
fix: ensure payment request button only shows for submitted invoices (backport #50970)
2025-12-09 17:21:53 +05:30
Diptanil Saha
67c5249b38 Merge pull request #50988 from frappe/mergify/bp/version-15-hotfix/pr-50968 2025-12-09 17:18:10 +05:30
Diptanil Saha
af067d1c00 chore: resolve conflict 2025-12-09 17:17:27 +05:30
Abdeali Chharchhoda
b4053ee0d8 fix: ensure payment request button only shows for submitted invoices
(cherry picked from commit f26ee9e546)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
2025-12-09 11:42:22 +00:00
rohitwaghchaure
9f846e2636 Merge pull request #50990 from frappe/mergify/bp/version-15-hotfix/pr-50978
fix: performance of the reposting (backport #50978)
2025-12-09 16:54:21 +05:30
Rohit Waghchaure
8d734df63b fix: performance of the reposting
(cherry picked from commit 1bcfad8eb1)
2025-12-09 11:06:16 +00:00
Abdeali Chharchhoda
0998123e52 refactor: payment request status updates with bulk database operation
(cherry picked from commit 5154fa8259)
2025-12-09 10:59:11 +00:00
mergify[bot]
f2c556a6cc fix: tds for customer and supplier in Journal Entry (backport #49963) (#50985)
Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: Smit Vora <smitvora203@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-12-09 16:26:36 +05:30
Khushi Rawat
b41612bea8 Merge pull request #50982 from khushi8112/missing-attribute-issue
fix: Missing attribute error
2025-12-09 15:31:08 +05:30
khushi8112
da88196a89 fix: remove comment 2025-12-09 15:13:40 +05:30
khushi8112
bde209b077 fix: missing attribute error when restoring asset 2025-12-09 15:10:16 +05:30
ljain112
1a278e7ca0 fix: handle duplicate description in item-wise report 2025-12-09 12:03:50 +05:30
rohitwaghchaure
1637cb4168 Merge pull request #50973 from frappe/mergify/bp/version-15-hotfix/pr-50972
fix: incorrect condition (backport #50972)
2025-12-08 20:27:10 +05:30
Rohit Waghchaure
d9e9f35230 fix: incorrect condition
(cherry picked from commit 264baf34f6)
2025-12-08 14:39:16 +00:00
rohitwaghchaure
cbc73148d3 Merge pull request #50969 from frappe/mergify/bp/version-15-hotfix/pr-50742
fix: inward same serial / batches in disassembly which were used (backport #50742)
2025-12-08 19:53:36 +05:30
rohitwaghchaure
60a18247e1 chore: fix conflicts 2025-12-08 19:09:52 +05:30
rohitwaghchaure
7cc0436083 chore: fix conflicts 2025-12-08 19:08:45 +05:30
rohitwaghchaure
f8eb48472e chore: fix conflicts 2025-12-08 19:07:13 +05:30
rohitwaghchaure
8074d396d0 chore: fix conflicts
Removed posting_datetime and type_of_transaction from the query.
2025-12-08 19:05:51 +05:30
Rohit Waghchaure
cfbd71693b fix: inward same serial / batches in disassembly which were used
(cherry picked from commit 95e6c72539)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-12-08 12:57:58 +00:00
Diptanil Saha
3a2d7d18a3 Merge pull request #50946 from frappe/mergify/bp/version-15-hotfix/pr-50931
fix(bulk transaction process): skip records creation if original records are marked 'On Hold' or 'Closed' (backport #50931)
2025-12-05 16:56:52 +05:30
Diptanil Saha
b55cefc54f Merge pull request #50945 from frappe/mergify/bp/version-15-hotfix/pr-50943
fix(sales invoice): 100% additional discount gl issue with discount accounting (backport #50943)
2025-12-05 16:48:44 +05:30
Diptanil Saha
05778bb81a chore: resolve conflict 2025-12-05 16:40:57 +05:30
Diptanil Saha
a70296e9b5 Merge pull request #50931 from diptanilsaha/gh-49357
(cherry picked from commit 31d55248e4)

# Conflicts:
#	erpnext/utilities/bulk_transaction.py
2025-12-05 11:03:16 +00:00
diptanilsaha
bd6210a212 fix(sales invoice): 100% additional discount gl issue with discount accounting
(cherry picked from commit d6bdbfe266)
2025-12-05 11:02:16 +00:00
Khushi Rawat
944c9ad0b3 Merge pull request #50924 from frappe/mergify/bp/version-15-hotfix/pr-50879
fix: remove set_only_once from is_fixed_asset field (backport #50879)
2025-12-04 13:04:50 +05:30
Khushi Rawat
bd00a484ea fix: conflicts 2025-12-04 12:48:50 +05:30
Khushi Rawat
1427b4ac3f fix: conflicts 2025-12-04 12:48:07 +05:30
ravibharathi656
fd6e42e15e fix: remove set_only_once from is_fixed_asset
(cherry picked from commit 70521fb9bf)

# Conflicts:
#	erpnext/stock/doctype/item/item.json
#	erpnext/stock/doctype/item/item.py
2025-12-04 06:44:46 +00:00
rohitwaghchaure
8b071c0d22 Merge pull request #50922 from frappe/mergify/bp/version-15-hotfix/pr-50913
fix: variant items not fetched while making BOM for Variant Item (backport #50913)
2025-12-04 11:43:57 +05:30
Rohit Waghchaure
176ce0d4d6 fix: variant items not fetched while making BOM for Variant Item
(cherry picked from commit a0256bd798)
2025-12-04 04:38:17 +00:00
rohitwaghchaure
dd888fc30a Merge pull request #50909 from frappe/mergify/bp/version-15-hotfix/pr-50905
fix: LCV is not changing the valuation of the repacked item (backport #50905)
2025-12-04 10:06:31 +05:30
Mihir Kandoi
789adaeabe Merge pull request #50908 from frappe/mergify/bp/version-15-hotfix/pr-50906
fix: untranslated string in job card (backport #50906)
2025-12-03 20:05:11 +05:30
rohitwaghchaure
2342f8d710 chore: fix conflicts
Removed test for purchase expense account and repost GL entries.
2025-12-03 18:38:40 +05:30
Rohit Waghchaure
8b22d9d95e fix: LCV is not changing the valuation of the repacked item
(cherry picked from commit ccbbc60585)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2025-12-03 12:58:22 +00:00
rohitwaghchaure
626c799b60 Merge pull request #50907 from frappe/mergify/bp/version-15-hotfix/pr-50902
fix: fg qty uom in manufacture entry (backport #50902)
2025-12-03 18:22:15 +05:30
Mihir Kandoi
b2f6d07c25 fix: untranslated string in job card
(cherry picked from commit ec06f4a71b)
2025-12-03 12:40:35 +00:00
Mihir Kandoi
70d57260d6 fix: fg qty uom in manufacture entry
(cherry picked from commit d9a377108c)
2025-12-03 12:37:19 +00:00
Khushi Rawat
9062b90237 Merge pull request #50887 from 0xD0M1M0/patch-1
fix: Reduce asset value on asset capitalization cancelation
2025-12-03 15:01:41 +05:30
rohitwaghchaure
40467bc26c Merge pull request #50901 from frappe/mergify/bp/version-15-hotfix/pr-50896
fix: quality inspection showing Not Saved (backport #50896)
2025-12-03 14:52:51 +05:30
Rohit Waghchaure
abe599a49d fix: quality inspection showing Not Saved
(cherry picked from commit 3f78d6afed)
2025-12-03 08:08:37 +00:00
rohitwaghchaure
9975f5fe69 Merge pull request #50889 from frappe/mergify/bp/version-15-hotfix/pr-50888
fix: cost center not reset (backport #50888)
2025-12-02 22:37:08 +05:30
Frappe PR Bot
dab17c194c chore(release): Bumped to Version 15.91.0
# [15.91.0](https://github.com/frappe/erpnext/compare/v15.90.1...v15.91.0) (2025-12-02)

### Bug Fixes

* add validation for cancelled reposting entries ([085d685](085d685488))
* add validation for company linked address fields ([0aed8c0](0aed8c04c6))
* **barcode_scanner:** set serial and batch before item to prevent FIFO override ([7d7f929](7d7f929cfc))
* conflicts ([199e25e](199e25ec06))
* correct field name for subcontracted items in material request ([4b49080](4b49080bc4))
* do not override source document in serial no ([69c6b2f](69c6b2f463))
* **email campaign:** send emails using bcc ([b660b90](b660b90adc))
* **Employee:** add/delete user permission (backport [#47016](https://github.com/frappe/erpnext/issues/47016)) ([#50761](https://github.com/frappe/erpnext/issues/50761)) ([821f3f5](821f3f5884))
* enhance SalesOrderController setup method to call super.setup ([7805ccf](7805ccf176))
* exclude is_group records ([a444325](a444325bd1))
* include accounting dimensions in stock entries created during asset repair. ([26872c3](26872c3c25))
* incorrect positional param for `get_field_precision` util (backport [#50764](https://github.com/frappe/erpnext/issues/50764)) ([#50795](https://github.com/frappe/erpnext/issues/50795)) ([ff1ca9d](ff1ca9d480))
* item price not considering based on valid_upto ([dfda8e6](dfda8e6241))
* **Job Card:** avoid Type Error when completed_qty is None ([#50447](https://github.com/frappe/erpnext/issues/50447)) ([cac9eed](cac9eed306))
* label for warehouse based on material request type ([8ee7c47](8ee7c47fdf))
* mandatory depends on for the rejected inventory dimension field ([8c62080](8c620802f0))
* negative batch in subcontracting receipt ([5def006](5def006033))
* **payment reconciliation:** added a hint that posting date can be changed on exchange gain/loss reconcile dialog ([0e03607](0e0360781e))
* **payment-recon:** add validation for outstanding of dr_cr ([70feb50](70feb500f6))
* **pos:** add negative stock validation for product bundle ([46a49a1](46a49a134d))
* remove unused translation files (<100 lines) ([7f7c5f2](7f7c5f2381))
* resolve conflict ([bd795f5](bd795f5546))
* **stock entry:** use fg item expense account for direct manufacturing entry ([4ca5e9e](4ca5e9eef8))
* two primary buttons ([1d2fccf](1d2fccfc0b))
* use asset in against_voucher while posting gl entries for capitalized asset repairs ([80642ed](80642edf4f))
* use posting_date instead of bill_date from purchase invoice ([c12a560](c12a560c63))

### Features

* add stock uom read only field to stock reconciliation item doctype ([5711225](57112258e6))
2025-12-02 16:31:39 +00:00
Rohit Waghchaure
8a3148eee6 fix: cost center not reset
(cherry picked from commit 29f2ecbd6f)
2025-12-02 16:30:41 +00:00
Diptanil Saha
1f79242366 Merge pull request #50868 from frappe/version-15-hotfix 2025-12-02 22:00:12 +05:30
Diptanil Saha
3f673a6848 Merge pull request #50886 from frappe/mergify/bp/version-15-hotfix/pr-50882
fix: mandatory depends on for the rejected inventory dimension field (backport #50882)
2025-12-02 21:26:26 +05:30
Diptanil Saha
293f114c9d Merge pull request #50847 from barredterra/rm-unused-translations 2025-12-02 21:19:55 +05:30
Diptanil Saha
252cc89ec7 Merge pull request #50871 from frappe/mergify/bp/version-15-hotfix/pr-50846 2025-12-02 21:19:40 +05:30
Diptanil Saha
3eaccfe201 Merge pull request #50873 from frappe/mergify/bp/version-15-hotfix/pr-50773
fix: add validation for cancelled reposting entries (backport #50773)
2025-12-02 21:15:56 +05:30
Diptanil Saha
653bb1072f Merge pull request #50885 from frappe/mergify/bp/version-15-hotfix/pr-50864
fix: exclude is_group records (backport #50864)
2025-12-02 21:14:35 +05:30
Diptanil Saha
0a64e43e92 chore: resolve linter issue 2025-12-02 21:04:00 +05:30
diptanilsaha
020db922b7 chore: resolve conflicts 2025-12-02 20:56:54 +05:30
rohitwaghchaure
a67a11e933 Merge pull request #50884 from rohitwaghchaure/fixed-donot-override-source
fix: do not override source document in serial no
2025-12-02 20:55:22 +05:30
Rohit Waghchaure
8c620802f0 fix: mandatory depends on for the rejected inventory dimension field
(cherry picked from commit 5daa625fe8)
2025-12-02 15:10:40 +00:00
0xD0M1M0
a31fb2ac6c fix: Adjust asset purchase amounts based on docstatus
allows cancelation
2025-12-02 16:09:56 +01:00
ravibharathi656
a444325bd1 fix: exclude is_group records
(cherry picked from commit e08805128b)

# Conflicts:
#	erpnext/setup/doctype/customer_group/customer_group.json
#	erpnext/setup/doctype/item_group/item_group.json
#	erpnext/setup/doctype/supplier_group/supplier_group.json
#	erpnext/setup/doctype/territory/territory.json
2025-12-02 15:07:39 +00:00
rohitwaghchaure
2f4b1341d2 Merge pull request #50878 from frappe/mergify/bp/version-15-hotfix/pr-50808
fix(stock entry): use fg item expense account for direct manufacturing entry (backport #50808)
2025-12-02 20:37:39 +05:30
Diptanil Saha
7640944bb9 Merge pull request #50881 from frappe/mergify/bp/version-15-hotfix/pr-50372
fix: add validation for company linked address field (backport #50372)
2025-12-02 20:36:22 +05:30
Rohit Waghchaure
69c6b2f463 fix: do not override source document in serial no 2025-12-02 20:34:55 +05:30
Pugazhendhi Velu
ef6f2389a0 test: add minimal test case
(cherry picked from commit e64b6db2eb)
2025-12-02 14:45:03 +00:00
Pugazhendhi Velu
1d4b97c619 test: add test for company linked address fields
(cherry picked from commit e10007c646)
2025-12-02 14:45:02 +00:00
Pugazhendhi Velu
0aed8c04c6 fix: add validation for company linked address fields
(cherry picked from commit 800a44a65f)
2025-12-02 14:45:02 +00:00
rohitwaghchaure
b1b6953aed chore: fix conflicts 2025-12-02 17:44:17 +05:30
rohitwaghchaure
b2ea5620b2 chore: fix conflicts
Removed the expense account assignment for subcontracting delivery.
2025-12-02 17:42:18 +05:30
rohitwaghchaure
743b179b08 Merge pull request #50877 from frappe/mergify/bp/version-15-hotfix/pr-50850
fix(barcode_scanner): set serial and batch before item to prevent FIFO override (backport #50850)
2025-12-02 17:41:31 +05:30
Pugazhendhi Velu
4553d04c38 test: add test for fg item expense account in direct manufacturing
(cherry picked from commit ba2411b4ee)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
2025-12-02 12:03:37 +00:00
Pugazhendhi Velu
4ca5e9eef8 fix(stock entry): use fg item expense account for direct manufacturing entry
(cherry picked from commit ce1312764f)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-12-02 12:03:37 +00:00
Pugazhendhi Velu
7d7f929cfc fix(barcode_scanner): set serial and batch before item to prevent FIFO override
(cherry picked from commit 92ec633a5c)
2025-12-02 12:02:36 +00:00
l0gesh29
085d685488 fix: add validation for cancelled reposting entries
(cherry picked from commit d8fc369e38)
2025-12-02 11:47:36 +00:00
Diptanil Saha
0458c548ec chore: resolve conflict 2025-12-02 17:14:00 +05:30
Sudharsanan11
e5457f8bb7 test(pos): add test for product bundle negative stock validation
(cherry picked from commit 2612152456)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.py
2025-12-02 11:39:09 +00:00
Sudharsanan11
46a49a134d fix(pos): add negative stock validation for product bundle
(cherry picked from commit 38b4536300)
2025-12-02 11:39:08 +00:00
Khushi Rawat
25a9327b14 Merge pull request #50862 from frappe/mergify/bp/version-15-hotfix/pr-50794
fix: use asset in against_voucher while posting gl entries for capitalised asset repairs (backport #50794)
2025-12-02 12:45:20 +05:30
Mihir Kandoi
d0d38214c5 Merge pull request #50790 from Abdeali099/fix-incorrect-fieldname 2025-12-02 12:30:48 +05:30
Khushi Rawat
991c46d058 Merge pull request #50858 from frappe/mergify/bp/version-15-hotfix/pr-50793
fix: include accounting dimensions in stock entries created during asset repair. (backport #50793)
2025-12-02 12:23:10 +05:30
rohitwaghchaure
ca9bd8b499 Merge pull request #50845 from frappe/mergify/bp/version-15-hotfix/pr-50844
fix: label for warehouse based on material request type (backport #50844)
2025-12-02 12:22:18 +05:30
Khushi Rawat
199e25ec06 fix: conflicts 2025-12-02 12:20:48 +05:30
Navin-S-R
f38fb68d62 chore: reload asset doc before assertEqual
(cherry picked from commit 8c35a6ecdd)
2025-12-02 06:37:54 +00:00
Navin-S-R
3a22d29d7b test: add unit test to validate capitalized asset repair gl entries being booked against the asset
(cherry picked from commit bcf6deec9a)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/test_asset_repair.py
2025-12-02 06:37:54 +00:00
Navin S R
80642edf4f fix: use asset in against_voucher while posting gl entries for capitalized asset repairs
(cherry picked from commit a7e43eddad)
2025-12-02 06:37:54 +00:00
ljain112
9a3e1058f6 refactor: show_general ledger for consistency with other doctyoes
(cherry picked from commit cdbe8b909b)
2025-12-02 06:27:21 +00:00
ljain112
26872c3c25 fix: include accounting dimensions in stock entries created during asset repair.
(cherry picked from commit 147a5ee953)
2025-12-02 06:27:21 +00:00
rohitwaghchaure
dba3f3d335 chore: fix conflicts 2025-12-02 11:04:27 +05:30
barredterra
7f7c5f2381 fix: remove unused translation files (<100 lines)
These translate <=1% of available strings, so cannot be deemed useful.
2025-12-01 17:38:13 +01:00
Rohit Waghchaure
8ee7c47fdf fix: label for warehouse based on material request type
(cherry picked from commit 699e9b4452)

# Conflicts:
#	erpnext/stock/doctype/material_request/material_request.js
2025-12-01 15:58:39 +00:00
Raffael Meyer
441a2bcf38 chore: backport translations from develop (#50842) 2025-12-01 14:50:04 +00:00
mergify[bot]
821f3f5884 fix(Employee): add/delete user permission (backport #47016) (#50761)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-12-01 12:49:37 +01:00
Diptanil Saha
ca70e8e9a6 Merge pull request #50825 from frappe/mergify/bp/version-15-hotfix/pr-50797
fix(payment-recon): add validation for outstanding of dr_cr (backport #50797)
2025-12-01 13:27:15 +05:30
l0gesh29
70feb500f6 fix(payment-recon): add validation for outstanding of dr_cr
(cherry picked from commit 765f9a9bbf)
2025-12-01 07:41:28 +00:00
rohitwaghchaure
bf8b3d0546 Merge pull request #50818 from frappe/mergify/bp/version-15-hotfix/pr-50799
fix: negative batch in subcontracting receipt (backport #50799)
2025-12-01 12:50:42 +05:30
Diptanil Saha
07d8bc7852 Merge pull request #50819 from frappe/mergify/bp/version-15-hotfix/pr-50814
fix(email campaign): send emails using bcc (backport #50814)
2025-12-01 12:30:55 +05:30
diptanilsaha
b660b90adc fix(email campaign): send emails using bcc
(cherry picked from commit 7e8d19b0c8)
2025-12-01 06:17:15 +00:00
Rohit Waghchaure
5def006033 fix: negative batch in subcontracting receipt
(cherry picked from commit 71e46b3ef5)
2025-12-01 06:15:37 +00:00
Khushi Rawat
ade6acccfb Merge pull request #50800 from frappe/mergify/bp/version-15-hotfix/pr-50772
fix: use posting_date instead of bill_date from purchase invoice (backport #50772)
2025-11-29 00:28:11 +05:30
Khushi Rawat
bd795f5546 fix: resolve conflict 2025-11-28 17:26:33 +05:30
Navin S R
c12a560c63 fix: use posting_date instead of bill_date from purchase invoice
(cherry picked from commit 145d40dec8)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py
2025-11-28 11:49:33 +00:00
mergify[bot]
ff1ca9d480 fix: incorrect positional param for get_field_precision util (backport #50764) (#50795) 2025-11-28 08:50:03 +00:00
Abdeali Chharchhoda
4b49080bc4 fix: correct field name for subcontracted items in material request 2025-11-28 12:33:38 +05:30
Diptanil Saha
64e6b36d04 Merge pull request #50789 from frappe/mergify/bp/version-15-hotfix/pr-50642
fix(payment reconciliation): added a hint that posting date can be changed on exchange gain/loss reconcile dialog (backport #50642)
2025-11-28 11:54:32 +05:30
Jatin3128
0e0360781e fix(payment reconciliation): added a hint that posting date can be changed on exchange gain/loss reconcile dialog
(cherry picked from commit 4b612c64a8)
2025-11-28 06:22:00 +00:00
Mihir Kandoi
c62be10620 Merge pull request #50778 from frappe/mergify/bp/version-15-hotfix/pr-50777 2025-11-27 17:28:07 +05:30
Mihir Kandoi
fa541a2604 chore: make unnecessary field read only and show only when required
(cherry picked from commit aab7cd1ae6)
2025-11-27 11:41:54 +00:00
Diptanil Saha
5590b8d40b Merge pull request #50558 from efeone/pos_rate_issue 2025-11-27 16:33:50 +05:30
Mihir Kandoi
a32165016d Merge pull request #50774 from mihir-kandoi/gh50218 2025-11-27 14:56:44 +05:30
Mihir Kandoi
57112258e6 feat: add stock uom read only field to stock reconciliation item doctype 2025-11-27 14:39:19 +05:30
rohitwaghchaure
0bec404e69 Merge pull request #50770 from frappe/mergify/bp/version-15-hotfix/pr-50769
fix: two primary buttons (backport #50769)
2025-11-27 12:06:10 +05:30
Rohit Waghchaure
1d2fccfc0b fix: two primary buttons
(cherry picked from commit f68515210b)
2025-11-27 06:27:09 +00:00
Raffael Meyer
cac9eed306 fix(Job Card): avoid Type Error when completed_qty is None (#50447) 2025-11-26 13:16:47 +01:00
Frappe PR Bot
2bf12a6683 chore(release): Bumped to Version 15.90.1
## [15.90.1](https://github.com/frappe/erpnext/compare/v15.90.0...v15.90.1) (2025-11-26)

### Bug Fixes

* enhance SalesOrderController setup method to call super.setup ([38c4453](38c44533b3))
2025-11-26 09:12:23 +00:00
Diptanil Saha
6205be5e73 Merge pull request #50755 from frappe/mergify/bp/version-15/pr-50754
fix: enhance SalesOrderController setup method to call super.setup (backport #50752)
2025-11-26 14:40:58 +05:30
ljain112
38c44533b3 fix: enhance SalesOrderController setup method to call super.setup
(cherry picked from commit 563c2998ca)
(cherry picked from commit 7805ccf176)
2025-11-26 09:08:27 +00:00
Diptanil Saha
a6713b176b Merge pull request #50754 from frappe/mergify/bp/version-15-hotfix/pr-50752
fix: enhance SalesOrderController setup method to call super.setup (backport #50752)
2025-11-26 14:35:21 +05:30
ljain112
7805ccf176 fix: enhance SalesOrderController setup method to call super.setup
(cherry picked from commit 563c2998ca)
2025-11-26 09:02:56 +00:00
Frappe PR Bot
a66ce02520 chore(release): Bumped to Version 15.90.0
# [15.90.0](https://github.com/frappe/erpnext/compare/v15.89.2...v15.90.0) (2025-11-25)

### Bug Fixes

* add filter company and status to job card employee ([015f946](015f946a14))
* add missing translate function ([475eada](475eada727))
* add return status for purchase receipt ([8ccb9a5](8ccb9a5ad2))
* add validation for FG Items as per BOM qty (backport [#50579](https://github.com/frappe/erpnext/issues/50579)) ([#50715](https://github.com/frappe/erpnext/issues/50715)) ([1995291](1995291194))
* apply precision for scrap items amount ([5b60fbb](5b60fbbd30))
* **customer:** link contact and addresses if created from lead/opportunity/prospect ([b1d40de](b1d40de87e))
* ignore reserved batches from total available batches ([673b893](673b893942))
* incorrect query filter when selecting primary customer adr ([#50727](https://github.com/frappe/erpnext/issues/50727)) ([e8e09cf](e8e09cf8ea))
* **ledger-summary-report:** show party group and territory ([56f03ae](56f03aee02))
* **manufacturing:** apply precision for bom amount and rm_cost_per_qty ([2678694](2678694c5f))
* pick list status doesn't update when DN created from it and PL was created from SO ([2809c46](2809c46a6e))
* prevent pi status from changing on asset repair ([3f2081b](3f2081b440))
* pricing rule was ignoring time validity ([f62e5e6](f62e5e69b8))
* **product bundle:** fields reset if doc is new ([4ba4da0](4ba4da090d))
* **purchase_receipt:** add internal_and_external_links field to show purchase invoice connection count ([89fcdbf](89fcdbf56b))
* redundant message on bom save ([5b16740](5b1674018b))
* remove disabled warehouse in get_warehouses_based_on_account ([aa94c91](aa94c91c12))
* serial batch selector shown only once ([25cd230](25cd230471))
* show current company warehouse only in get material from bom MR ([1d6e3e4](1d6e3e4e7d))
* tests ([45bc218](45bc218acb))
* unhide zero val checkbox ([a247337](a24733791d))
* unknown column error ([2e9a0cb](2e9a0cb01c))
* use current_tax_amount value for base_total_taxes_and_charges ([7ed3c6d](7ed3c6d18a))
* validate sabb autocreation when disabled ([85c0c16](85c0c16964))
* validation for SABB deletion ([0bc98b6](0bc98b609f))

### Features

* **accounting-dimension:** add dynamic triggers for custom accounting dimensions ([#50621](https://github.com/frappe/erpnext/issues/50621)) ([2b7d586](2b7d58602d))
* modify accounting dimension as multiselect field ([6b6e017](6b6e017e36))
* **reports:** preserve accounting dimension filters while navigating between reports ([02a1f81](02a1f815da))
2025-11-25 15:02:30 +00:00
ruthra kumar
3c43b42a01 Merge pull request #50741 from frappe/version-15-hotfix
chore: release v15
2025-11-25 20:31:01 +05:30
ruthra kumar
8ed3a0ec65 Merge branch 'version-15' into version-15-hotfix 2025-11-25 20:14:37 +05:30
mergify[bot]
488d635dc9 chore: switched frankfurter domain from frankfurter.app to frankfurter.dev (backport #50734) (#50740)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-11-25 15:49:53 +05:30
ruthra kumar
08d230b3e3 Merge pull request #50731 from frappe/mergify/bp/version-15-hotfix/pr-50561
fix(ledger-summary-report): show party group and territory (backport #50561)
2025-11-25 12:33:49 +05:30
Mihir Kandoi
f383fafb15 Merge pull request #50728 from frappe/mergify/bp/version-15-hotfix/pr-50727
fix: incorrect query filter when selecting primary customer adr (backport #50727)
2025-11-25 12:14:17 +05:30
ruthra kumar
38124a7616 chore: resolve conflicts 2025-11-25 12:01:37 +05:30
l0gesh29
56cf5382f0 test: add party_group, territory in json
(cherry picked from commit 8f91919933)

# Conflicts:
#	erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py
2025-11-25 05:38:57 +00:00
l0gesh29
56f03aee02 fix(ledger-summary-report): show party group and territory
(cherry picked from commit 231479a6e2)

# Conflicts:
#	erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
2025-11-25 05:38:57 +00:00
Mihir Kandoi
841f5c24ad chore: resolve conflicts 2025-11-25 10:51:39 +05:30
ruthra kumar
e8051ba180 Merge pull request #50729 from frappe/mergify/bp/version-15-hotfix/pr-50621
feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (backport #50621)
2025-11-25 10:26:47 +05:30
Logesh Periyasamy
2b7d58602d feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (#50621)
* feat: add dynamic triggers for custom accounting dimensions

* feat: add accounting dimension trigger call in setup event

* chore: ignore cur_frm semgrep rules

* chore: move function to transaction.js

(cherry picked from commit 5e58e344b2)
2025-11-25 04:51:32 +00:00
Mihir Kandoi
e8e09cf8ea fix: incorrect query filter when selecting primary customer adr (#50727)
(cherry picked from commit c2b8b97d7d)

# Conflicts:
#	erpnext/selling/doctype/customer/customer.json
2025-11-25 04:49:20 +00:00
Khushi Rawat
188c633d6e Merge pull request #50544 from aerele/asset-repair-pi-status
fix: prevent pi status from changing on asset repair
2025-11-24 23:28:33 +05:30
mergify[bot]
1995291194 fix: add validation for FG Items as per BOM qty (backport #50579) (#50715)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com>
fix: add validation for FG Items as per BOM qty (#50579)
2025-11-24 11:18:18 +00:00
Mihir Kandoi
bb0d75eb78 Merge pull request #50717 from frappe/mergify/bp/version-15-hotfix/pr-50716
fix: add missing translate function (backport #50716)
2025-11-24 13:53:48 +05:30
El-Shafei H.
475eada727 fix: add missing translate function
(cherry picked from commit 56def01240)
2025-11-24 07:39:09 +00:00
mergify[bot]
6cffba9a71 Merge pull request #50713 from frappe/mergify/bp/version-15-hotfix/pr-50712 2025-11-24 07:17:43 +00:00
Mihir Kandoi
35f9f9f330 Merge pull request #50711 from frappe/mergify/bp/version-15-hotfix/pr-50661
fix(manufacturing): apply precision for bom amount and rm_cost_per_qty (backport #50661)
2025-11-24 11:02:50 +05:30
Pugazhendhi Velu
5b60fbbd30 fix: apply precision for scrap items amount
(cherry picked from commit 9194e6350a)
2025-11-24 05:08:42 +00:00
Pugazhendhi Velu
2678694c5f fix(manufacturing): apply precision for bom amount and rm_cost_per_qty
(cherry picked from commit 57f9353d90)
2025-11-24 05:08:42 +00:00
Mihir Kandoi
b6a80da457 Merge pull request #50709 from frappe/mergify/bp/version-15-hotfix/pr-50707
fix: unknown column error (backport #50707)
2025-11-23 19:54:21 +05:30
Mihir Kandoi
2e9a0cb01c fix: unknown column error
(cherry picked from commit 3b7d7aed4c)
2025-11-23 14:08:48 +00:00
Frappe PR Bot
38c1867ade chore(release): Bumped to Version 15.89.2
## [15.89.2](https://github.com/frappe/erpnext/compare/v15.89.1...v15.89.2) (2025-11-21)

### Bug Fixes

* use current_tax_amount value for base_total_taxes_and_charges ([c082eda](c082edabf4))
2025-11-21 17:34:52 +00:00
Diptanil Saha
a48b999af9 Merge pull request #50691 from frappe/mergify/bp/version-15/pr-50690
fix: use current_tax_amount value for base_total_taxes_and_charges (backport #50476) (backport #50690)
2025-11-21 23:03:28 +05:30
Pugazhendhi Velu
c082edabf4 fix: use current_tax_amount value for base_total_taxes_and_charges
(cherry picked from commit 5a3fcbedb5)
(cherry picked from commit 7ed3c6d18a)
2025-11-21 17:16:54 +00:00
Diptanil Saha
60ec7d0fb8 Merge pull request #50690 from frappe/mergify/bp/version-15-hotfix/pr-50476
fix: use current_tax_amount value for base_total_taxes_and_charges (backport #50476)
2025-11-21 22:31:13 +05:30
Pugazhendhi Velu
7ed3c6d18a fix: use current_tax_amount value for base_total_taxes_and_charges
(cherry picked from commit 5a3fcbedb5)
2025-11-21 16:41:52 +00:00
Frappe PR Bot
b7e4fb9d83 chore(release): Bumped to Version 15.89.1
## [15.89.1](https://github.com/frappe/erpnext/compare/v15.89.0...v15.89.1) (2025-11-21)

### Bug Fixes

* ignore reserved batches from total available batches ([64950d3](64950d39b5))
2025-11-21 10:36:37 +00:00
rohitwaghchaure
e49e7b621d Merge pull request #50668 from frappe/mergify/bp/version-15/pr-50612
fix: ignore reserved batches from total available batches (backport #50612)
2025-11-21 16:05:05 +05:30
Mihir Kandoi
0a67d20ff8 Merge pull request #50671 from frappe/mergify/bp/version-15-hotfix/pr-50667
fix: pricing rule was ignoring time validity (backport #50667)
2025-11-21 13:08:25 +05:30
Mihir Kandoi
83c6d861eb Merge pull request #50670 from frappe/mergify/bp/version-15-hotfix/pr-50655
fix: pick list status doesn't update when DN created from it and PL w... (backport #50655)
2025-11-21 13:00:31 +05:30
Mihir Kandoi
f62e5e69b8 fix: pricing rule was ignoring time validity
(cherry picked from commit ffae7c4175)
2025-11-21 07:22:07 +00:00
Mihir Kandoi
45bc218acb fix: tests
(cherry picked from commit d26f8aa629)
2025-11-21 07:14:00 +00:00
Mihir Kandoi
2809c46a6e fix: pick list status doesn't update when DN created from it and PL was created from SO
(cherry picked from commit f7b3253683)
2025-11-21 07:14:00 +00:00
Kavin
15c41178d0 test: add unit test for reserved stock validation
(cherry picked from commit 55f2f1c515)
2025-11-21 06:46:17 +00:00
Kavin
64950d39b5 fix: ignore reserved batches from total available batches
(cherry picked from commit 673b893942)
2025-11-21 06:46:17 +00:00
Diptanil Saha
ff1b83025a Merge pull request #50666 from frappe/mergify/bp/version-15-hotfix/pr-50665
fix(customer): link contact and addresses if created from lead/opportunity/prospect (backport #50665)
2025-11-21 07:03:57 +05:30
diptanilsaha
b1d40de87e fix(customer): link contact and addresses if created from lead/opportunity/prospect
(cherry picked from commit 310099f4cd)
2025-11-21 01:18:18 +00:00
Mihir Kandoi
532031c21d Merge pull request #50650 from frappe/mergify/bp/version-15-hotfix/pr-50385
fix: remove disabled warehouse in get_warehouses_based_on_account (backport #50385)
2025-11-20 17:44:02 +05:30
Mihir Kandoi
cb70efb8ed Merge pull request #50653 from frappe/mergify/bp/version-15-hotfix/pr-50639
fix(product bundle): fields reset if doc is new (backport #50639)
2025-11-20 17:43:43 +05:30
Mihir Kandoi
ca0b4696ba Merge pull request #50652 from frappe/mergify/bp/version-15-hotfix/pr-50649
fix: unhide zero val checkbox in stock reco (backport #50649)
2025-11-20 17:43:26 +05:30
Mihir Kandoi
4ba4da090d fix(product bundle): fields reset if doc is new
(cherry picked from commit 7faee7edc2)
2025-11-20 10:42:53 +00:00
rohitwaghchaure
e3b2cc24b2 chore: fix conflicts 2025-11-20 16:12:41 +05:30
Mihir Kandoi
a24733791d fix: unhide zero val checkbox
(cherry picked from commit 20e0313a8c)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
2025-11-20 10:41:55 +00:00
Mihir Kandoi
aa94c91c12 fix: remove disabled warehouse in get_warehouses_based_on_account
(cherry picked from commit ff2d9bf4cb)
2025-11-20 10:33:54 +00:00
Mihir Kandoi
87ffbdf129 Merge pull request #50641 from frappe/mergify/bp/version-15-hotfix/pr-50502
fix(purchase_receipt): add internal_and_external_links field to show … (backport #50502)
2025-11-20 15:45:43 +05:30
rohitwaghchaure
5ef7b8c526 Merge pull request #50648 from frappe/mergify/bp/version-15-hotfix/pr-50646
fix: serial batch selector shown only once (backport #50646)
2025-11-20 15:12:51 +05:30
Mihir Kandoi
25cd230471 fix: serial batch selector shown only once
(cherry picked from commit aa6f09e9a9)
2025-11-20 09:38:37 +00:00
rohitwaghchaure
f05933e814 Merge pull request #50645 from frappe/mergify/bp/version-15-hotfix/pr-50644
fix: validation for SABB deletion (backport #50644)
2025-11-20 14:33:05 +05:30
Diptanil Saha
8df2612694 Merge pull request #50643 from frappe/mergify/bp/version-15-hotfix/pr-50289 2025-11-20 13:58:58 +05:30
Rohit Waghchaure
0bc98b609f fix: validation for SABB deletion
(cherry picked from commit dd4bef0706)
2025-11-20 08:19:48 +00:00
l0gesh29
02a1f815da feat(reports): preserve accounting dimension filters while navigating between reports
(cherry picked from commit fcfcaa76c6)
2025-11-20 07:46:55 +00:00
l0gesh29
6b6e017e36 feat: modify accounting dimension as multiselect field
(cherry picked from commit 3fcd8d84ac)
2025-11-20 07:46:55 +00:00
Karuppasamy B
89fcdbf56b fix(purchase_receipt): add internal_and_external_links field to show purchase invoice connection count
(cherry picked from commit 6c1620ab8c)
2025-11-20 07:03:55 +00:00
rohitwaghchaure
eb2571492f Merge pull request #50612 from aerele/fix/validate-reserved-stock
fix: ignore reserved batches from total available batches
2025-11-19 22:16:00 +05:30
Kavin
55f2f1c515 test: add unit test for reserved stock validation 2025-11-19 17:51:10 +05:30
Kavin
673b893942 fix: ignore reserved batches from total available batches 2025-11-19 17:51:10 +05:30
mergify[bot]
c85ce55f27 Merge pull request #50631 from frappe/mergify/bp/version-15-hotfix/pr-50629
fix: process loss % can be negative (backport #50629)
2025-11-19 11:30:39 +00:00
Mihir Kandoi
a19252e3b3 Merge pull request #50628 from frappe/mergify/bp/version-15-hotfix/pr-50627
fix: show current company warehouse only in get material from bom MR (backport #50627)
2025-11-19 16:29:17 +05:30
Mihir Kandoi
7ece6fd558 Merge pull request #50626 from frappe/mergify/bp/version-15-hotfix/pr-50625
fix: add filter company and status to job card employee (backport #50625)
2025-11-19 16:24:08 +05:30
Mihir Kandoi
1d6e3e4e7d fix: show current company warehouse only in get material from bom MR
(cherry picked from commit 3271eaaf0e)
2025-11-19 10:46:47 +00:00
Mihir Kandoi
015f946a14 fix: add filter company and status to job card employee
(cherry picked from commit 3ca3a6d9bb)
2025-11-19 10:42:32 +00:00
rohitwaghchaure
533a2dbc32 Merge pull request #50607 from frappe/mergify/bp/version-15-hotfix/pr-50512
fix: add return status for purchase receipt (backport #50512)
2025-11-19 14:18:12 +05:30
rohitwaghchaure
99aeb8ecd1 Merge pull request #50606 from frappe/mergify/bp/version-15-hotfix/pr-50486
fix: validate sabb autocreation when disabled (backport #50486)
2025-11-19 14:18:00 +05:30
Mihir Kandoi
371030f8d4 Merge pull request #50615 from frappe/mergify/bp/version-15-hotfix/pr-50614
fix: redundant message on bom save (backport #50614)
2025-11-19 12:53:50 +05:30
Mihir Kandoi
2550b44db8 chore: resolve conflicts 2025-11-19 12:38:47 +05:30
Mihir Kandoi
5b1674018b fix: redundant message on bom save
(cherry picked from commit 074f07694f)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/bom.py
2025-11-19 07:06:11 +00:00
Frappe PR Bot
d8dab986fa chore(release): Bumped to Version 15.89.0
# [15.89.0](https://github.com/frappe/erpnext/compare/v15.88.1...v15.89.0) (2025-11-19)

### Bug Fixes

* add cancelled option in status field ([623a0a9](623a0a932e))
* add condition for allow negative stock in pos (backport [#50369](https://github.com/frappe/erpnext/issues/50369)) ([#50600](https://github.com/frappe/erpnext/issues/50600)) ([2d6640a](2d6640ac61))
* add doctype parameter to lead details for correct company details ([f0eac47](f0eac47037))
* **asset repair:** validate pi status ([2db91ee](2db91ee67e))
* back calcalute total amount from rate and tax_amount in tax withholding details report ([5728299](57282999ad))
* construct batch_nos and serial_nos to avoid NoneType error ([0a0177c](0a0177cb9e))
* correct profit after tax calculation by reducing expenses from income ([627b34a](627b34a120))
* current qty in stock reco ([b4b8459](b4b8459f2c))
* enable allow_negative_stock settings ([2a5c9b4](2a5c9b469c))
* **financial reports:** set fiscal year associated with the default company ([ac40b59](ac40b59665))
* first and last name in supplier quick entry (backport [#50510](https://github.com/frappe/erpnext/issues/50510)) ([#50514](https://github.com/frappe/erpnext/issues/50514)) ([3b636d5](3b636d5db7))
* **general_ledger:** add translation for accounting dimension ([799119a](799119ad3e))
* handle NoneType object error for product bundle ([2b7abfb](2b7abfb34b))
* improve precision in tax amount calculations in tax withholding details report ([c150e57](c150e5795e))
* on changes of paid from/to account fetch company bank account ([3d8a344](3d8a344173))
* **period closing voucher:** add title to error log ([#50498](https://github.com/frappe/erpnext/issues/50498)) ([33962ac](33962ac995))
* prevent pos opening entry creation for disabled pos profile ([68747b5](68747b5818))
* **stock-entry:** prevent default warehouse from overriding parent warehouse ([a5ec0e4](a5ec0e4f50))
* unintended backported depends_on expression ([#50529](https://github.com/frappe/erpnext/issues/50529)) ([81a1628](81a16286a1))
* use dynamic account type to get average ratio balance ([a2c82b4](a2c82b4dc3))

### Features

* Add first and last name fields to quick entry customer creation (backport [#46281](https://github.com/frappe/erpnext/issues/46281)) ([#50522](https://github.com/frappe/erpnext/issues/50522)) ([8c98f16](8c98f1692a))
* **Company:** allow setting default sales contact, fetch into sales transaction (backport [#50159](https://github.com/frappe/erpnext/issues/50159)) ([#50599](https://github.com/frappe/erpnext/issues/50599)) ([f8294f1](f8294f1754))
* **Item Price:** validate UOM ([376da8d](376da8df0a))
* **pos:** prevent disabling POS Profile when open POS sessions exist ([87e8305](87e8305753))
2025-11-19 02:58:17 +00:00
Diptanil Saha
f25e2295d0 Merge pull request #50595 from frappe/version-15-hotfix 2025-11-19 08:26:48 +05:30
ruthra kumar
b4fd4812cd Merge pull request #50527 from aerele/default-report-fiscal-year-v15
fix(financial reports): set fiscal year associated with the default company
2025-11-18 18:12:21 +05:30
rohitwaghchaure
876dec5077 chore: fix conflicts 2025-11-18 18:05:08 +05:30
rohitwaghchaure
36e9aae9d0 chore: fix conflicts 2025-11-18 18:04:29 +05:30
Pugazhendhi Velu
8ccb9a5ad2 fix: add return status for purchase receipt
(cherry picked from commit 3a0e1e8ef9)
2025-11-18 12:33:18 +00:00
Kavin
85c0c16964 fix: validate sabb autocreation when disabled
(cherry picked from commit 3ca1940881)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-11-18 12:29:01 +00:00
ruthra kumar
6a46045804 Merge pull request #50603 from frappe/mergify/bp/version-15-hotfix/pr-50524
fix: use dynamic account type to get average ratio balance (backport #50524)
2025-11-18 17:44:31 +05:30
ruthra kumar
befa4bef0d Merge pull request #50601 from frappe/mergify/bp/version-15-hotfix/pr-50516
fix(general_ledger): add translation for accounting dimension (backport #50516)
2025-11-18 17:13:34 +05:30
mergify[bot]
f8294f1754 feat(Company): allow setting default sales contact, fetch into sales transaction (backport #50159) (#50599)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-18 17:05:58 +05:30
mergify[bot]
2d6640ac61 fix: add condition for allow negative stock in pos (backport #50369) (#50600)
Co-authored-by: Logesh Periyasamy <logeshperiyasamy24@gmail.com>
fix: add condition for allow negative stock in pos (#50369)
2025-11-18 17:05:47 +05:30
Navin-S-R
627b34a120 fix: correct profit after tax calculation by reducing expenses from income
(cherry picked from commit f420371a7e)
2025-11-18 11:22:38 +00:00
Navin-S-R
a2c82b4dc3 fix: use dynamic account type to get average ratio balance
(cherry picked from commit 9118f08e7b)
2025-11-18 11:22:38 +00:00
Logesh Periyasamy
799119ad3e fix(general_ledger): add translation for accounting dimension
(cherry picked from commit 113ff17c71)
2025-11-18 11:15:37 +00:00
ruthra kumar
a4a0a2a0fb Merge pull request #50554 from frappe/mergify/bp/version-15-hotfix/pr-50540
fix(stock-entry): prevent default warehouse from overriding parent warehouse (backport #50540)
2025-11-18 15:09:08 +05:30
ruthra kumar
35fb2b8ede Merge pull request #50573 from frappe/mergify/bp/version-15-hotfix/pr-50496
fix: back calcalute total amount from rate and tax_amount in tax withholding details report (backport #50496)
2025-11-18 15:08:26 +05:30
ruthra kumar
c844bf5547 Merge pull request #50574 from frappe/mergify/bp/version-15-hotfix/pr-50439
fix: add doctype parameter to lead details for correct company details (backport #50439)
2025-11-18 15:07:15 +05:30
Kavin
81a16286a1 fix: unintended backported depends_on expression (#50529)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
2025-11-18 10:07:26 +02:00
Khushi Rawat
3c8534c4cc Merge pull request #50571 from aerele/fix/support-52685
fix: add cancelled option in status field
2025-11-18 11:15:53 +05:30
ravibharathi656
ac40b59665 fix(financial reports): set fiscal year associated with the default company 2025-11-18 11:14:40 +05:30
ravibharathi656
3f2081b440 fix: prevent pi status from changing on asset repair 2025-11-18 11:09:25 +05:30
Pugazhendhi Velu
2db91ee67e fix(asset repair): validate pi status 2025-11-17 14:06:26 +00:00
ljain112
f0eac47037 fix: add doctype parameter to lead details for correct company details
(cherry picked from commit 0b91338771)
2025-11-17 13:39:09 +00:00
ljain112
4df80c5b53 chore: typo in comment
(cherry picked from commit e056c0327d)
2025-11-17 13:36:08 +00:00
ljain112
c150e5795e fix: improve precision in tax amount calculations in tax withholding details report
(cherry picked from commit 7c5f5405cc)
2025-11-17 13:36:08 +00:00
ljain112
57282999ad fix: back calcalute total amount from rate and tax_amount in tax withholding details report
(cherry picked from commit d3751d9bb4)
2025-11-17 13:36:08 +00:00
Pugazhendhi Velu
623a0a932e fix: add cancelled option in status field 2025-11-17 13:09:38 +00:00
rohitwaghchaure
9b06eaab78 Merge pull request #50535 from aerele/support-53360
fix: construct batch_nos and serial_nos to avoid NoneType error
2025-11-17 16:24:20 +05:30
Sherin KR
dfda8e6241 fix: item price not considering based on valid_upto 2025-11-17 14:34:26 +05:30
Kavin
2a5c9b469c fix: enable allow_negative_stock settings 2025-11-17 13:53:55 +05:30
Kavin
0a0177cb9e fix: construct batch_nos and serial_nos to avoid NoneType error 2025-11-17 12:35:16 +05:30
Mihir Kandoi
eed144d7c9 Merge pull request #50231 from frappe/mergify/bp/version-15-hotfix/pr-40586 2025-11-17 10:26:43 +05:30
Pugazhendhi Velu
a5ec0e4f50 fix(stock-entry): prevent default warehouse from overriding parent warehouse
(cherry picked from commit 8b38578914)
2025-11-17 04:41:27 +00:00
mergify[bot]
8c98f1692a feat: Add first and last name fields to quick entry customer creation (backport #46281) (#50522)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: maasanto <73234812+maasanto@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-13 21:58:34 +05:30
Diptanil Saha
182e84e94c Merge pull request #50521 from frappe/mergify/bp/version-15-hotfix/pr-50498
fix(period closing voucher): add title to error log (backport #50498)
2025-11-13 21:50:21 +05:30
PUGAZHENDHI V
33962ac995 fix(period closing voucher): add title to error log (#50498)
(cherry picked from commit 4f720b3969)
2025-11-13 15:56:37 +00:00
mergify[bot]
3b636d5db7 fix: first and last name in supplier quick entry (backport #50510) (#50514)
Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-13 15:37:50 +05:30
Frappe PR Bot
70b8b3bb9e chore(release): Bumped to Version 15.88.1
## [15.88.1](https://github.com/frappe/erpnext/compare/v15.88.0...v15.88.1) (2025-11-12)

### Bug Fixes

* handle NoneType object error for product bundle ([eab6d69](eab6d69ec9))
2025-11-12 18:26:04 +00:00
rohitwaghchaure
c42954bec7 Merge pull request #50500 from frappe/mergify/bp/version-15/pr-50488
fix: handle NoneType object error for packed_items (backport #50488)
2025-11-12 23:54:34 +05:30
Kavin
eab6d69ec9 fix: handle NoneType object error for product bundle
(cherry picked from commit 2b7abfb34b)
2025-11-12 17:21:25 +00:00
rohitwaghchaure
3f0bea2d9f Merge pull request #50488 from aerele/support-53076
fix: handle NoneType object error for packed_items
2025-11-12 22:50:37 +05:30
rohitwaghchaure
34ed9b455f Merge pull request #50491 from frappe/mergify/bp/version-15-hotfix/pr-50487
fix: current qty in stock reconciliation  (backport #50487)
2025-11-12 22:49:44 +05:30
Kavin
2b7abfb34b fix: handle NoneType object error for product bundle 2025-11-12 18:26:19 +05:30
Diptanil Saha
2ba1731d3f Merge pull request #50484 from frappe/mergify/bp/version-15-hotfix/pr-50409 2025-11-12 15:14:53 +05:30
Rohit Waghchaure
b4b8459f2c fix: current qty in stock reco
(cherry picked from commit 58315bc963)
2025-11-12 08:26:01 +00:00
Diptanil Saha
d5160c4c86 test: delete outdated pos opening entry 2025-11-12 12:26:26 +05:30
Diptanil Saha
af19b81343 chore: resolve conflict 2025-11-12 11:35:02 +05:30
Diptanil Saha
650d2f74ba chore: resolve conflict 2025-11-12 11:33:56 +05:30
diptanilsaha
68747b5818 fix: prevent pos opening entry creation for disabled pos profile
(cherry picked from commit e35e8968f0)

# Conflicts:
#	erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
2025-11-12 05:44:01 +00:00
diptanilsaha
38848ff43b test: added test to validate disabled pos profile
(cherry picked from commit 69016a284f)

# Conflicts:
#	erpnext/accounts/doctype/pos_profile/test_pos_profile.py
2025-11-12 05:44:00 +00:00
diptanilsaha
87e8305753 feat(pos): prevent disabling POS Profile when open POS sessions exist
(cherry picked from commit c5219278fb)
2025-11-12 05:44:00 +00:00
Diptanil Saha
a2345d467e Merge pull request #50483 from frappe/mergify/bp/version-15-hotfix/pr-50323
fix(payment entry): on changes of paid from/to account fetch company bank account (backport #50323)
2025-11-12 11:11:37 +05:30
Abdeali Chharchhoda
3d8a344173 fix: on changes of paid from/to account fetch company bank account
(cherry picked from commit 4901dc2531)
2025-11-12 05:38:56 +00:00
Raffael Meyer
7d607b82f1 chore: resolve conflicts 2025-10-29 15:37:04 +01:00
barredterra
376da8df0a feat(Item Price): validate UOM
(cherry picked from commit 69824eff80)

# Conflicts:
#	erpnext/stock/doctype/item_price/item_price.py
2025-10-27 06:32:01 +00:00
182 changed files with 29333 additions and 1088 deletions

View File

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

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
} else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-18 08:32:26.895076",
"modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,8 +141,9 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.app":
elif self.service_provider == "frankfurter.dev":
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:

View File

@@ -420,7 +420,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

View File

@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
)
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
debit_or_credit = (
"debit_in_account_currency"
if self.voucher_type == "Credit Note"
else "credit_in_account_currency"
)
rev_debit_or_credit = (
"credit_in_account_currency"
if debit_or_credit == "debit_in_account_currency"
else "debit_in_account_currency"
)
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(
d.get(debit_or_credit)
for d in self.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
)
party_amount = sum(
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
)
inv = frappe._dict(
{
party_type: parties[0],
"doctype": doctype,
"company": self.company,
"posting_date": self.posting_date,
"net_total": net_total,
}
)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
accounts = []
for d in self.get("accounts"):
if d.get("account") == tax_withholding_details.get("account_head"):
d.update(
{
"account": tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get("tax_amount"),
}
)
accounts.append(d.get("account"))
if d.get("account") == party_account:
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append(
"accounts",
{
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
},
)
to_remove = [
d
for d in self.get("accounts")
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
]
for d in to_remove:
self.remove(d)
JournalEntryTaxWithholding(self).apply()
def update_asset_value(self):
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
frappe.throw(_("Accounts table cannot be blank."))
class JournalEntryTaxWithholding:
def __init__(self, journal_entry):
self.doc: JournalEntry = journal_entry
self.party = None
self.party_type = None
self.party_account = None
self.party_row = None
self.existing_tds_rows = []
self.precision = None
self.has_multiple_parties = False
# Direction fields based on party type
self.party_field = None # "credit" for Supplier, "debit" for Customer
self.reverse_field = None # opposite of party_field
def apply(self):
if not self._set_party_info():
return
self._setup_direction_fields()
self._reset_existing_tds()
if not self._should_apply_tds():
self._cleanup_duplicate_tds_rows(None)
return
if self.has_multiple_parties:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
net_total = self._calculate_net_total()
if net_total <= 0:
return
tds_details = self._get_tds_details(net_total)
if not tds_details or not tds_details.get("tax_amount"):
return
self._create_or_update_tds_row(tds_details)
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
self._recalculate_totals()
def _should_apply_tds(self):
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
def _set_party_info(self):
for row in self.doc.get("accounts"):
if row.party_type in ("Customer", "Supplier") and row.party:
if self.party and row.party != self.party:
self.has_multiple_parties = True
if not self.party:
self.party = row.party
self.party_type = row.party_type
self.party_account = row.account
self.party_row = row
if row.get("is_tax_withholding_account"):
self.existing_tds_rows.append(row)
return bool(self.party)
def _setup_direction_fields(self):
"""
For Supplier (TDS): party has credit, TDS reduces credit
For Customer (TCS): party has debit, TCS increases debit
"""
if self.party_type == "Supplier":
self.party_field = "credit"
self.reverse_field = "debit"
else: # Customer
self.party_field = "debit"
self.reverse_field = "credit"
self.precision = self.doc.precision(self.party_field, self.party_row)
def _reset_existing_tds(self):
for row in self.existing_tds_rows:
# TDS amount is always in credit (liability to government)
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
if not tds_amount:
continue
self._update_party_amount(tds_amount, is_reversal=True)
# zero_out_tds_row
row.update(
{
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
}
)
def _update_party_amount(self, amount, is_reversal=False):
amount = flt(amount, self.precision)
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
# Determine which field the party amount is in
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
# If amount is in reverse field, flip the signs
if active_field == self.reverse_field:
amount = -amount
amount_in_party_currency = -amount_in_party_currency
# Direction multiplier based on party type:
# Customer (TCS): +1 (add to debit)
# Supplier (TDS): -1 (subtract from credit)
direction = 1 if self.party_type == "Customer" else -1
# Reversal inverts the direction
if is_reversal:
direction = -direction
adjustment = amount * direction
adjustment_in_party_currency = amount_in_party_currency * direction
active_field_account_currency = f"{active_field}_in_account_currency"
self.party_row.update(
{
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
active_field_account_currency: flt(
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
self.precision,
),
}
)
def _calculate_net_total(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
account_type_map = get_account_type_map(self.doc.company)
return flt(
sum(
d.get(self.reverse_field) - d.get(self.party_field)
for d in self.doc.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
and d.account != self.party_account
and not d.get("is_tax_withholding_account")
),
self.precision,
)
def _get_tds_details(self, net_total):
return get_party_tax_withholding_details(
frappe._dict(
{
"party_type": self.party_type,
"party": self.party,
"doctype": self.doc.doctype,
"company": self.doc.company,
"posting_date": self.doc.posting_date,
"tax_withholding_net_total": net_total,
"base_tax_withholding_net_total": net_total,
"grand_total": net_total,
}
),
self.doc.tax_withholding_category,
)
def _create_or_update_tds_row(self, tds_details):
tax_account = tds_details.get("account_head")
account_currency = get_account_currency(tax_account)
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
# Find existing TDS row for this account
tax_row = None
for row in self.doc.get("accounts"):
if row.account == tax_account and row.get("is_tax_withholding_account"):
tax_row = row
break
if not tax_row:
tax_row = self.doc.append(
"accounts",
{
"account": tax_account,
"account_currency": account_currency,
"exchange_rate": exchange_rate,
"cost_center": tds_details.get("cost_center"),
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
"is_tax_withholding_account": 1,
},
)
# TDS/TCS is always credited (liability to government)
tax_row.update(
{
"credit": tax_amount,
"credit_in_account_currency": tax_amount_in_account_currency,
"debit": 0,
"debit_in_account_currency": 0,
}
)
self._cleanup_duplicate_tds_rows(tax_row)
def _cleanup_duplicate_tds_rows(self, current_tax_row):
rows_to_remove = [
row
for row in self.doc.get("accounts")
if row.get("is_tax_withholding_account") and row != current_tax_row
]
for row in rows_to_remove:
self.doc.remove(row)
def _recalculate_totals(self):
self.doc.set_amounts_in_company_currency()
self.doc.set_total_debit_credit()
self.doc.set_against_account()
@frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -34,6 +34,7 @@
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"is_tax_withholding_account",
"col_break3",
"is_advance",
"user_remark",
@@ -282,12 +283,19 @@
"options": "advance_voucher_type",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-10-27 13:48:32.805100",
"modified": "2025-11-27 12:23:33.157655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
debit_in_account_currency: DF.Currency
exchange_rate: DF.Float
is_advance: DF.Literal["No", "Yes"]
is_tax_withholding_account: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -592,6 +592,8 @@ frappe.ui.form.on("Payment Entry", {
paid_from: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_from,
@@ -609,6 +611,8 @@ frappe.ui.form.on("Payment Entry", {
paid_to: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_to,
@@ -1350,6 +1354,8 @@ frappe.ui.form.on("Payment Entry", {
},
bank_account: function (frm) {
if (frm.set_company_bank_account_based_on_coa) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
frappe.call({
@@ -1388,6 +1394,34 @@ frappe.ui.form.on("Payment Entry", {
}
},
set_company_bank_account: function (frm) {
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (!frm.doc.company || !frm.doc[field]) return;
frm.set_company_bank_account_based_on_coa = true;
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Bank Account",
filters: {
company: frm.doc.company,
account: frm.doc[field],
disabled: 0,
},
fieldname: ["name"],
},
callback: async function (r) {
if (r.message) await frm.set_value("bank_account", r.message.name);
frm.set_company_bank_account_based_on_coa = false;
},
});
},
sales_taxes_and_charges_template: function (frm) {
frm.trigger("fetch_taxes_from_template");
},

View File

@@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController):
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
self.base_total_taxes_and_charges += current_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@@ -334,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
{
fieldtype: "HTML",
options: "<b> New Journal Entry will be posted for the difference amount </b>",
options: __(
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
).bold(),
},
],
primary_action: () => {

View File

@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
)
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
reconcile_dr_or_cr = (

View File

@@ -850,6 +850,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
doc_updates = {}
for ref in references:
if not ref.payment_request:
@@ -875,7 +876,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
title=_("Invalid Allocated Amount"),
)
# update status
# determine status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
@@ -883,12 +884,15 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
elif new_outstanding_amount > 0:
status = "Partially Paid"
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
# prepare bulk update data
doc_updates[ref.payment_request] = {
"outstanding_amount": new_outstanding_amount,
"status": status,
}
# bulk update all payment requests
if doc_updates:
frappe.db.bulk_update("Payment Request", doc_updates)
def get_dummy_message(doc):

View File

@@ -475,8 +475,15 @@ def process_gl_and_closing_entries(doc):
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name))
frappe.db.set_value(
doc.doctype,
doc.name,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def process_cancellation(voucher_type, voucher_no):
@@ -488,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no):
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
frappe.log_error(
title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no)
)
frappe.db.set_value(
voucher_type,
voucher_no,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def delete_closing_entries(voucher_no):

View File

@@ -18,12 +18,17 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.stock_ledger import is_negative_stock_allowed
class PartialPaymentValidationError(frappe.ValidationError):
pass
class ProductBundleStockValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -189,6 +194,9 @@ class POSInvoice(SalesInvoice):
super().__init__(*args, **kwargs)
def validate(self):
if not self.customer:
frappe.throw(_("Please select Customer first"))
if not cint(self.is_pos):
frappe.throw(
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
@@ -345,34 +353,69 @@ class POSInvoice(SalesInvoice):
):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
if frappe.db.exists("Product Bundle", d.item_code):
(
availability,
is_stock_item,
is_negative_stock_allowed,
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
else:
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
if is_negative_stock_allowed:
continue
if isinstance(availability, list):
error_msgs = []
for item in availability:
if flt(item["available"]) < flt(item["required"]):
error_msgs.append(
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
frappe.bold(item["item_code"]),
frappe.bold(flt(item["required"], 2)),
frappe.bold(flt(item["available"], 2)),
)
)
if error_msgs:
frappe.throw(
_(
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
).format(
d.idx,
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
"<br>".join(error_msgs),
),
title=_("Insufficient Stock for Product Bundle Items"),
exc=ProductBundleStockValidationError,
)
else:
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
if is_stock_item and flt(availability) <= 0:
frappe.throw(
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
d.idx, item_code, warehouse
),
title=_("Item Out of Stock"),
)
elif is_stock_item and flt(availability) < flt(d.stock_qty):
frappe.throw(
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
d.idx,
item_code,
warehouse,
frappe.bold(flt(availability, 2)),
frappe.bold(flt(d.stock_qty, 2)),
),
title=_("Insufficient Stock"),
)
def validate_serialised_or_batched_item(self):
error_msg = []
@@ -765,15 +808,35 @@ def get_stock_availability(item_code, warehouse):
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
return get_bundle_availability(item_code, warehouse), is_stock_item, False
else:
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
return 0, is_stock_item, False
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
is_stock_item = True
bundle = frappe.get_doc("Product Bundle", item_code)
availabilities = []
for bundle_item in bundle.items:
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
available = bin_qty - reserved_qty
availabilities.append(
{
"item_code": bundle_item.item_code,
"required": bundle_item.qty * item_qty,
"available": available,
}
)
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
def get_bundle_availability(bundle_item_code, warehouse):

View File

@@ -964,6 +964,84 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
frappe.set_user("Administrator")
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
init_user_and_profile()
frappe.set_user("Administrator")
warehouse = "_Test Warehouse - _TC"
company = "_Test Company"
# Create stock sub-items
sub_item_a = "_Test Bundle SubA"
if not frappe.db.exists("Item", sub_item_a):
create_item(
item_code=sub_item_a,
is_stock_item=1,
)
sub_item_b = "_Test Bundle SubB"
if not frappe.db.exists("Item", sub_item_b):
create_item(
item_code=sub_item_b,
is_stock_item=1,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
if not frappe.db.exists("Item", bundle_item):
create_item(
item_code=bundle_item,
is_stock_item=0,
)
if not frappe.db.exists("Product Bundle", bundle_item):
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
pos_inv_sufficient = create_pos_invoice(
item=bundle_item,
qty=1,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
pos_inv_sufficient.insert()
pos_inv_sufficient.submit()
pos_inv_sufficient.cancel()
pos_inv_sufficient.delete()
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
pos_inv_insufficient = create_pos_invoice(
item=bundle_item,
qty=2,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
pos_inv_insufficient.save()
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
frappe.set_user("test@example.com")
def create_pos_invoice(**args):
args = frappe._dict(args)

View File

@@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater):
self.set_status()
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
if not frappe.db.exists("POS Profile", self.pos_profile):
frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile))
pos_profile_company, pos_profile_disabled = frappe.db.get_value(
"POS Profile", self.pos_profile, ["company", "disabled"]
)
if pos_profile_disabled:
frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile)))
if self.company != pos_profile_company:
frappe.throw(
_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
_("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company)
)
if not cint(frappe.db.get_value("User", self.user, "enabled")):

View File

@@ -70,6 +70,7 @@ class POSProfile(Document):
# end: auto-generated types
def validate(self):
self.validate_disabled()
self.validate_default_profile()
self.validate_all_link_fields()
self.validate_duplicate_groups()
@@ -94,6 +95,21 @@ class POSProfile(Document):
title=_("Mandatory Accounting Dimension"),
)
def validate_disabled(self):
old_doc = self.get_doc_before_save()
if (
old_doc
and self.disabled
and old_doc.disabled != self.disabled
and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"})
):
frappe.throw(
_("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format(
frappe.bold(self.name)
)
)
def validate_default_profile(self):
for row in self.applicable_for_users:
res = frappe.db.sql(

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import (
get_child_nodes,
@@ -38,6 +39,51 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001")
if pos_profile:
self.assertEqual(pos_profile.disabled, 1)
def test_disabled_pos_profile_after_opening(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
test_user, pos_profile = init_user_and_profile()
if pos_profile:
create_opening_entry(pos_profile, test_user.name)
self.assertEqual(pos_profile.disabled, 0)
pos_profile.disabled = 1
self.assertRaises(frappe.ValidationError, pos_profile.save)
def test_disabled_pos_profile_after_completing_session(self):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import (
create_opening_entry,
)
test_user, pos_profile = init_user_and_profile()
frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name})
if pos_profile:
opening_entry = create_opening_entry(pos_profile, test_user.name)
closing_entry = make_closing_entry_from_opening(opening_entry)
closing_entry.submit()
pos_profile.disabled = 1
pos_profile.save()
pos_profile.reload()
self.assertEqual(pos_profile.disabled, 1)
def get_customers_list(pos_profile=None):
if pos_profile is None:
@@ -117,6 +163,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
"disabled": cint(args.disabled) or 0,
}
)

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
values["transaction_date"] = date
if args.get("doctype") in [
"Quotation",

View File

@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
@@ -125,8 +126,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
cur_frm.add_custom_button(
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();

View File

@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {

View File

@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
)
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
if (
flt(item.base_net_amount, item.precision("base_net_amount"))
or item.is_fixed_asset
or enable_discount_accounting
):
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue

View File

@@ -85,6 +85,9 @@ def get_party_details(inv):
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
elif inv.doctype == "Journal Entry":
party_type = inv.party_type
party = inv.party
else:
party_type = "Supplier"
party = inv.supplier
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
party_type, parties, inv, tax_details, posting_date, pan_no
)
if party_type == "Supplier":
if party_type == "Supplier" or inv.doctype == "Journal Entry":
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
elif party_type == "Customer":
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
if inv.doctype == "Sales Invoice":
tax_amount = 0
else:
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount

View File

@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
def test_tds_on_journal_entry_for_supplier(self):
"""Test TDS deduction for Supplier in Debit Note"""
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
)
jv = make_journal_entry_with_tax_withholding(
party_type="Supplier",
party="Test TDS Supplier",
voucher_type="Debit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TDS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TDS on Journal Entry for Supplier"
jv.save()
jv.submit()
# TDS = 50000 * 10% = 5000
self.assertEqual(len(jv.accounts), 3)
# Find TDS account row
tds_row = None
supplier_row = None
for row in jv.accounts:
if row.account == "TDS - _TC":
tds_row = row
elif row.party == "Test TDS Supplier":
supplier_row = row
self.assertEqual(tds_row.credit, 5000)
self.assertEqual(tds_row.debit, 0)
# Supplier amount should be reduced by TDS
self.assertEqual(supplier_row.credit, 45000)
jv.cancel()
def test_tcs_on_journal_entry_for_customer(self):
"""Test TCS collection for Customer in Credit Note"""
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
# Create Credit Note with amount exceeding threshold
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party="Test TCS Customer",
voucher_type="Credit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TCS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TCS on Journal Entry for Customer"
jv.save()
jv.submit()
# Assert TCS calculation (10% on amount above threshold of 30000)
self.assertEqual(len(jv.accounts), 3)
# Find TCS account row
tcs_row = None
customer_row = None
for row in jv.accounts:
if row.account == "TCS - _TC":
tcs_row = row
elif row.party == "Test TCS Customer":
customer_row = row
# TCS should be credited (liability to government)
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
self.assertEqual(tcs_row.debit, 0)
# Customer amount should be increased by TCS
self.assertEqual(customer_row.debit, 52000)
jv.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
return pe
def make_journal_entry_with_tax_withholding(
party_type,
party,
voucher_type,
amount,
cost_center=None,
posting_date=None,
save=True,
submit=False,
):
"""Helper function to create Journal Entry for tax withholding"""
if not cost_center:
cost_center = "_Test Cost Center - _TC"
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or today()
jv.company = "_Test Company"
jv.voucher_type = voucher_type
jv.multi_currency = 0
if party_type == "Supplier":
# Debit Note: Expense Dr, Supplier Cr
expense_account = "Stock Received But Not Billed - _TC"
party_account = "Creditors - _TC"
jv.append(
"accounts",
{
"account": expense_account,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
else: # Customer
# Credit Note: Customer Dr, Income Cr
party_account = "Debtors - _TC"
income_account = "Sales - _TC"
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": income_account,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
if save or submit:
jv.insert()
if submit:
jv.submit()
return jv
def create_records():
# create a new suppliers
for name in [

View File

@@ -289,7 +289,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(

View File

@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_account",

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.accounts.utils import (
build_qb_match_conditions,
get_advance_payment_doctypes,
@@ -994,11 +995,7 @@ class ReceivablePayableReport:
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
cost_center_list = [
center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
]
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self):

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
party_type = self.filters.party_type
doctype = qb.DocType(party_type)
party_details_fields = [
doctype.name.as_("party"),
f"{scrub(party_type)}_name",
f"{scrub(party_type)}_group",
]
if party_type == "Customer":
party_details_fields.append(doctype.territory)
conditions = self.get_party_conditions(doctype)
query = (
qb.from_(doctype)
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
.where(Criterion.all(conditions))
)
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
from frappe.desk.reportview import build_match_conditions
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
}
]
columns += [
{
"label": _("Opening Balance"),
@@ -213,35 +244,6 @@ class PartyLedgerSummaryReport:
},
]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns
def get_data(self):

View File

@@ -174,7 +174,7 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year))
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision)
@@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters)
avg_debtors, avg_creditors, avg_stock = (
avg_data.get("receivable"),

View File

@@ -566,6 +566,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
else:
update_value_in_dict(consolidated_gle, key, gle)
if filters.get("include_dimensions"):
dimensions = [*accounting_dimensions, "cost_center", "project"]
for dimension in dimensions:
if val := gle.get(dimension):
gle[dimension] = _(val)
for value in consolidated_gle.values():
update_value_in_dict(totals, "total", value)
update_value_in_dict(totals, "closing", value)

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -16,7 +15,7 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
get_group_by_and_display_fields,
get_tax_accounts,
)
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
from erpnext.accounts.report.utils import get_values_for_columns
def execute(filters=None):
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
tax_doctype="Purchase Taxes and Charges",
)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
data = []
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
total_tax += flt(item_tax.get("tax_amount"))

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import functions as fn
from frappe.utils import cstr, flt
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -102,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
if item_tax.get("is_other_charges"):
@@ -546,9 +536,10 @@ def get_tax_accounts(
import json
item_row_map = {}
tax_columns = []
tax_columns = {}
invoice_item_row = {}
itemised_tax = {}
scrubbed_description_map = {}
add_deduct_tax = "charge_type"
tax_amount_precision = (
@@ -605,9 +596,14 @@ def get_tax_accounts(
tax_amount,
) in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount:
scrubbed_description = scrubbed_description_map.get(description)
if not scrubbed_description:
scrubbed_description = frappe.scrub(description)
scrubbed_description_map[description] = scrubbed_description
if scrubbed_description not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(description)
tax_columns[scrubbed_description] = description
if item_wise_tax_detail:
try:
@@ -641,7 +637,7 @@ def get_tax_accounts(
else tax_value
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": tax_rate,
"tax_amount": tax_value,
@@ -653,7 +649,7 @@ def get_tax_accounts(
continue
elif charge_type == "Actual" and tax_amount:
for d in invoice_item_row.get(parent, []):
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": "NA",
"tax_amount": flt(
@@ -662,12 +658,14 @@ def get_tax_accounts(
}
)
tax_columns.sort()
for desc in tax_columns:
tax_columns_list = list(tax_columns.keys())
tax_columns_list.sort()
for scrubbed_desc in tax_columns_list:
desc = tax_columns[scrubbed_desc]
columns.append(
{
"label": _(desc + " Rate"),
"fieldname": frappe.scrub(desc + " Rate"),
"fieldname": f"{scrubbed_desc}_rate",
"fieldtype": "Float",
"width": 100,
}
@@ -676,7 +674,7 @@ def get_tax_accounts(
columns.append(
{
"label": _(desc + " Amount"),
"fieldname": frappe.scrub(desc + " Amount"),
"fieldname": f"{scrubbed_desc}_amount",
"fieldtype": "Currency",
"options": "currency",
"width": 100,
@@ -714,7 +712,7 @@ def get_tax_accounts(
},
]
return itemised_tax, tax_columns
return itemised_tax, tax_columns_list
def add_total_row(
@@ -807,5 +805,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
total_row["percent_gt"] += item["percent_gt"]
for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
total_row.setdefault(f"{tax}_amount", 0.0)
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
precision = get_currency_precision()
out = []
entries = {}
@@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
values = net_total_map.get((voucher_type, name))
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
total_amount = grand_total = base_total
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
(voucher_type, name)
)
else:
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
if tax_amount and rate:
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
grand_total = values[1]
base_total = values[2]
if voucher_type == "Purchase Invoice":
bill_no = values[3]
bill_date = values[4]
else:
total_amount += entry.credit

View File

@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: function () {
var company = frappe.query_report.get_filter_value("company");
return {
doctype: "Cost Center",
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Project",
},
{

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.report.financial_statements import (
filter_accounts,
filter_out_zero_value_rows,
get_cost_centers_with_children,
set_gl_entries_by_account,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
@@ -103,10 +104,6 @@ def get_data(filters):
opening_balances = get_opening_balances(filters, ignore_is_opening)
# add filter inside list so that the query in financial_statements.py doesn't break
if filters.project:
filters.project = [filters.project]
set_gl_entries_by_account(
filters.company,
filters.from_date,
@@ -270,18 +267,12 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
closing_balance.cost_center.isin(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
)
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
opening_balance = opening_balance.where(closing_balance.project.isin(filters.project))
if frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"):

View File

@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm");
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");

View File

@@ -371,7 +371,6 @@
"label": "Other Details"
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
@@ -379,7 +378,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -597,7 +596,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-10-23 22:43:33.634452",
"modified": "2025-11-17 18:01:51.417942",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -103,6 +103,7 @@ class Asset(AccountsController):
status: DF.Literal[
"Draft",
"Submitted",
"Cancelled",
"Partially Depreciated",
"Fully Depreciated",
"Sold",
@@ -1205,7 +1206,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
return {
"company": purchase_doc.company,
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
"purchase_date": purchase_doc.get("posting_date"),
"gross_purchase_amount": flt(first_item.base_net_amount),
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),

View File

@@ -537,6 +537,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.asset_doc = asset
asset_repair.modify_depreciation_schedule()
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)

View File

@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
self.update_target_asset()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -607,8 +608,12 @@ class AssetCapitalization(StockController):
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
if self.docstatus == 2:
asset_doc.gross_purchase_amount -= total_target_asset_value
asset_doc.purchase_amount -= 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()

View File

@@ -63,14 +63,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -134,6 +127,26 @@ frappe.ui.form.on("Asset Repair", {
frm.set_value("repair_cost", 0);
}
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
__("View")
);
}
},
});
frappe.ui.form.on("Asset Repair Consumed Item", {

View File

@@ -139,7 +139,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
"options": "Asset",
"reqd": 1
},
@@ -250,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-29 15:14:34.044564",
"modified": "2025-11-17 18:35:54.575265",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -6,6 +6,9 @@ from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -60,6 +63,17 @@ class AssetRepair(AccountsController):
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
self.validate_purchase_invoice_status()
def validate_purchase_invoice_status(self):
if self.purchase_invoice:
docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", self.purchase_invoice)
)
)
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
@@ -235,6 +249,12 @@ class AssetRepair(AccountsController):
)
stock_entry.asset_repair = self.name
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -246,8 +266,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -304,8 +323,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,

View File

@@ -4,6 +4,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
@@ -294,6 +296,31 @@ class TestAssetRepair(unittest.TestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations

View File

@@ -36,6 +36,7 @@
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
@@ -270,6 +271,14 @@
"label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
}
],
"grid_page_length": 50,
@@ -278,7 +287,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-08-20 22:13:38.506889",
"modified": "2025-11-20 12:59:09.925862",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -44,6 +44,7 @@ class BuyingSettings(Document):
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None
use_transaction_date_exchange_rate: DF.Check
validate_consumed_qty: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
erpnext.buying.BuyingController
) {
setup() {
this.setup_accounting_dimension_triggers();
this.frm.custom_make_buttons = {
"Purchase Receipt": "Purchase Receipt",
"Purchase Invoice": "Purchase Invoice",

View File

@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
frm.set_query("supplier_primary_contact", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
supplier: doc.name,
type: "Contact",
},
};
});
frm.set_query("supplier_primary_address", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
link_doctype: "Supplier",
link_name: doc.name,
supplier: doc.name,
type: "Address",
},
};
});
@@ -137,6 +139,14 @@ frappe.ui.form.on("Supplier", {
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
frm.set_query("supplier_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
get_supplier_group_details: function (frm) {
frappe.call({

View File

@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
contact = frappe.qb.DocType("Contact")
type = filters.get("type")
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link")
return (
frappe.qb.from_(contact)
query = (
frappe.qb.from_(type_doctype)
.join(dynamic_link)
.on(contact.name == dynamic_link.parent)
.select(contact.name, contact.email_id)
.on(type_doctype.name == dynamic_link.parent)
.select(type_doctype.name)
.where(
(dynamic_link.link_name == supplier)
& (dynamic_link.link_doctype == "Supplier")
& (contact.name.like(f"%{txt}%"))
& (type_doctype.name.like(f"%{txt}%"))
)
).run(as_dict=False)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()

View File

@@ -308,6 +308,52 @@ class AccountsController(TransactionBase):
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
self.validate_company_linked_addresses()
def validate_company_linked_addresses(self):
address_fields = []
sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")
if self.doctype in sales_doctypes:
address_fields = ["dispatch_address_name", "company_address"]
elif self.doctype in purchase_doctypes:
address_fields = ["billing_address", "shipping_address"]
if not address_fields:
return
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
for field in address_fields:
address = self.get(field)
if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship:
continue
if address and not frappe.db.exists(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": "Company",
"link_name": self.company,
},
):
frappe.throw(
_("{0} does not belong to the Company {1}.").format(
_(self.meta.get_label(field)), bold(self.company)
)
)
@staticmethod
def is_drop_ship(items):
return any(item.delivered_by_supplier for item in items)
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -362,6 +408,24 @@ class AccountsController(TransactionBase):
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
cancelled_entries = (
frappe.qb.from_(dt)
.select(dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
.run(as_dict=True)
)
if cancelled_entries:
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
frappe.throw(
_(
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
"Kindly delete these entries before continuing."
).format(self.name, entries)
)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)

View File

@@ -185,7 +185,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:

View File

@@ -95,6 +95,7 @@ class SellingController(StockController):
# set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
self.set_company_contact_person()
def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
@@ -137,6 +138,7 @@ class SellingController(StockController):
lead,
posting_date=self.get("transaction_date") or self.get("posting_date"),
company=self.company,
doctype=self.doctype,
)
)
@@ -149,6 +151,13 @@ class SellingController(StockController):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
def set_company_contact_person(self):
"""Set the Company's Default Sales Contact as Company Contact Person."""
if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person:
self.company_contact_person = frappe.get_cached_value(
"Company", self.company, "default_sales_contact"
)
def remove_shipping_charge(self):
if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)

View File

@@ -86,6 +86,7 @@ status_map = {
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],
@@ -93,6 +94,7 @@ status_map = {
["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",

View File

@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
get_available_serial_nos,
@@ -505,7 +506,7 @@ class SubcontractingController(StockController):
if item.get("serial_and_batch_bundle"):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -686,7 +687,11 @@ class SubcontractingController(StockController):
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
elif (
item_details.has_batch_no
and not row.serial_and_batch_bundle
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
):
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
@@ -711,6 +716,11 @@ class SubcontractingController(StockController):
)
consumed_qty -= d.get("qty")
def batch_has_not_available(self, batch_no, qty_required):
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
return batch_qty < qty_required
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
@@ -849,7 +859,7 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype or (
self.backflush_based_on == "BOM" or self.is_return
):
for bom_item in self.__get_materials_from_bom(
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.query_builder import functions
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
@@ -685,6 +686,22 @@ class calculate_taxes_and_totals:
discount_amount = self.doc.discount_amount or 0
grand_total = self.doc.grand_total
if self.doc.get("is_return") and self.doc.get("return_against"):
doctype = frappe.qb.DocType(self.doc.doctype)
result = (
frappe.qb.from_(doctype)
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
.where(
(doctype.return_against == self.doc.return_against)
& (doctype.is_return == 1)
& (doctype.docstatus == 1)
)
).run(as_dict=True)
total_return_discount = abs(result[0].get("total_return_discount") or 0)
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
(grand_total >= 0 and discount_amount > grand_total)

View File

@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_purchase_order,
prepare_data_for_internal_transfer,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -2428,3 +2431,48 @@ class TestAccountsController(FrappeTestCase):
# Second return should only get remaining discount (100 - 60 = 40)
self.assertEqual(return_si_2.discount_amount, -40)
def test_company_linked_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
company_address = make_address(
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
)
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
company_address.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.shipping_address = company_address.name
po.save()
po.billing_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, po.save)
po.billing_address = company_address.name
po.reload()
po.save()
si = make_sales_order(do_not_save=1, do_not_submit=1)
si.dispatch_address_name = supplier_billing.name
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].delivered_by_supplier = 1
si.items[0].supplier = "_Test Supplier"
si.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.items[0].delivered_by_supplier = 1
po.save()

View File

@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
recipients=recipient_list,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,

View File

@@ -432,7 +432,7 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None):
def get_lead_details(lead, posting_date=None, company=None, doctype=None):
if not lead:
return {}
@@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead", company=company)
set_address_details(out, lead, "Lead", doctype=doctype, company=company)
taxes_and_charges = set_taxes(
None,

View File

@@ -340,10 +340,7 @@ doc_events = {
"User": {
"after_insert": "frappe.contacts.doctype.contact.contact.update_contact",
"validate": "erpnext.setup.doctype.employee.employee.validate_employee_role",
"on_update": [
"erpnext.setup.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role",
],
"on_update": "erpnext.portal.utils.set_default_role",
},
"Communication": {
"on_update": [

View File

@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
);
has_template_rm.forEach((d) => {
let bom_qty = dialog.fields_dict.qty?.value || 1;
dialog.fields_dict.items.df.data.push({
item_code: d.item_code,
variant_item_code: "",
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
source_warehouse: d.source_warehouse,
operation: d.operation,
});

View File

@@ -548,12 +548,14 @@
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
"label": "% Process Loss"
"label": "% Process Loss",
"non_negative": 1
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"non_negative": 1,
"read_only": 1
},
{
@@ -591,7 +593,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_semi_finished_goods === 0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "Finished Goods based Operating Cost"
@@ -640,7 +641,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-10-29 17:43:12.966753",
"modified": "2025-11-19 16:17:15.925156",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -465,7 +465,7 @@ class BOM(WebsiteGenerator):
)
)
def get_rm_rate(self, arg):
def get_rm_rate(self, arg, notify=True):
"""Get raw material rate as per selected method, if bom exists takes bom cost"""
rate = 0
if not self.rm_cost_as_per:
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
),
alert=True,
)
else:
elif notify:
frappe.msgprint(
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
alert=True,
@@ -796,11 +796,14 @@ class BOM(WebsiteGenerator):
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier,
}
},
notify=False,
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
)
d.base_amount = d.amount * flt(self.conversion_rate)
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
self.quantity, self.precision("quantity")
@@ -823,7 +826,10 @@ class BOM(WebsiteGenerator):
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
d.precision("amount"),
)
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)

View File

@@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", {
return doc.status === "Complete" ? "green" : "orange";
}
});
frm.set_query("employee", () => {
return {
filters: {
company: frm.doc.company,
status: "Active",
},
};
});
},
refresh: function (frm) {

View File

@@ -178,17 +178,12 @@ class JobCard(Document):
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
frappe.throw(
_(
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
).format(bold(self.operation), form_link),
title=_("Extra Job Card Quantity"),
)
def set_sub_operations(self):
if not self.sub_operations and self.operation:
@@ -605,7 +600,7 @@ class JobCard(Document):
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += time_log.completed_qty
op_row.completed_qty += flt(time_log.completed_qty)
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
@@ -1064,14 +1059,16 @@ class JobCard(Document):
)
if row.completed_qty < current_operation_qty:
msg = f"""The completed quantity {bold(current_operation_qty)}
of an operation {bold(self.operation)} cannot be greater
than the completed quantity {bold(row.completed_qty)}
of a previous operation
{bold(row.operation)}.
"""
frappe.throw(_(msg))
frappe.throw(
_(
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
).format(
bold(current_operation_qty),
bold(self.operation),
bold(row.completed_qty),
bold(row.operation),
)
)
def validate_work_order(self):
if self.is_work_order_closed():

View File

@@ -1627,7 +1627,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"min_order_qty": item_master.min_order_qty,
"default_material_request_type": item_master.default_material_request_type,
"qty": planned_qty or 1,
"is_sub_contracted": item_master.is_subcontracted_item,
"is_sub_contracted": item_master.is_sub_contracted_item,
"item_code": item_master.name,
"description": item_master.description,
"stock_uom": item_master.stock_uom,

View File

@@ -209,7 +209,7 @@ frappe.ui.form.on("Work Order", {
if (not_completed && not_completed.length) {
frm.add_custom_button(__("Create Job Card"), () => {
frm.trigger("make_job_card");
}).addClass("btn-primary");
});
}
}
}
@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
if (
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
frm.doc.produced_qty > 0 &&
frm.doc.produced_qty > frm.doc.disassembled_qty
) {
frm.add_custom_button(
__("Disassemble Order"),
@@ -253,7 +254,7 @@ frappe.ui.form.on("Work Order", {
if (non_consumed_items && non_consumed_items.length) {
frm.add_custom_button(__("Return Components"), function () {
frm.trigger("create_stock_return_entry");
}).addClass("btn-primary");
});
}
}
},
@@ -406,7 +407,6 @@ frappe.ui.form.on("Work Order", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
@@ -863,24 +863,6 @@ erpnext.work_order = {
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frm.qty_prompt = frappe.prompt(
fields,

View File

@@ -1373,6 +1373,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
item_details = get_item_details(item, project)
if frappe.db.get_value("Item", item, "variant_of"):
if variant_bom := frappe.db.get_value(
"BOM",
{"item": item, "is_default": 1, "docstatus": 1},
):
bom_no = variant_bom
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.update(item_details)

View File

@@ -318,7 +318,7 @@
"type": "Link"
}
],
"modified": "2024-10-21 14:13:38.777556",
"modified": "2025-11-24 11:11:28.343568",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,7 +336,7 @@
"doc_view": "List",
"label": "Learn Manufacturing",
"type": "URL",
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
"url": "https://school.frappe.io/lms/courses/production-planning-and-execution"
},
{
"color": "Grey",

View File

@@ -426,3 +426,4 @@ erpnext.patches.v15_0.set_asset_status_if_not_already_set
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

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.service_provider = "frankfurter.dev"
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -1093,6 +1093,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.refresh_field("payment_schedule");
}
cost_center(doc) {
this.frm.doc.items.forEach((item) => {
item.cost_center = doc.cost_center;
});
this.frm.refresh_field("items");
}
due_date(doc, cdt, cdn) {
// due_date is to be changed, payment terms template and/or payment schedule must
// be removed as due_date is automatically changed based on payment terms
@@ -2749,6 +2757,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
]);
}
}
setup_accounting_dimension_triggers() {
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
callback: function (r) {
if (r.message && r.message[0]) {
let dimensions = r.message[0].map((d) => d.fieldname);
dimensions.forEach((dim) => {
// nosemgrep: frappe-semgrep-rules.rules.frappe-cur-frm-usage
cur_frm.cscript[dim] = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", dim);
};
});
}
},
});
}
};
erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {

View File

@@ -79,18 +79,35 @@ erpnext.financial_statements = {
},
open_general_ledger: function (data) {
if (!data.account && !data.accounts) return;
let project = $.grep(frappe.query_report.filters, function (e) {
let filters = frappe.query_report.filters;
let project = $.grep(filters, function (e) {
return e.df.fieldname == "project";
});
let cost_center = $.grep(filters, function (e) {
return e.df.fieldname == "cost_center";
});
frappe.route_options = {
account: data.account || data.accounts,
company: frappe.query_report.get_filter_value("company"),
from_date: data.from_date || data.year_start_date,
to_date: data.to_date || data.year_end_date,
project: project && project.length > 0 ? project[0].$input.val() : "",
project: project && project.length > 0 ? project[0].get_value() : "",
cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "",
};
filters.forEach((f) => {
if (f.df.fieldtype == "MultiSelectList") {
if (f.df.fieldname in frappe.route_options) return;
let value = f.get_value();
if (value && value.length > 0) {
frappe.route_options[f.df.fieldname] = value;
}
}
});
let report = "General Ledger";
if (["Payable", "Receivable"].includes(data.account_type)) {

View File

@@ -420,25 +420,36 @@ $.extend(erpnext.utils, {
if (!frappe.boot.setup_complete) {
return;
}
const today = frappe.datetime.get_today();
if (!date) {
date = frappe.datetime.get_today();
date = today;
}
let fiscal_year = "";
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
if (
frappe.boot.current_fiscal_year &&
date >= frappe.boot.current_fiscal_year[1] &&
date <= frappe.boot.current_fiscal_year[2]
) {
if (with_dates) fiscal_year = frappe.boot.current_fiscal_year;
else fiscal_year = frappe.boot.current_fiscal_year[0];
} else if (today != date) {
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
type: "GET", // make it cacheable
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
}
return fiscal_year;
},

View File

@@ -138,15 +138,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => 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),
() => this.set_warehouse(row),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row),

View File

@@ -16,11 +16,13 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
insert() {
/**
* Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields.
* Therefor, resulting in the fields being "hidden".
* This results in the fields being "hidden".
*/
const map_field_names = {
email_address: "email_id",
mobile_number: "mobile_no",
map_to_first_name: "first_name",
map_to_last_name: "last_name",
};
Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => {
@@ -38,15 +40,27 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
label: __("Primary Contact Details"),
collapsible: 1,
},
{
label: __("First Name"),
fieldname: "map_to_first_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
label: __("Last Name"),
fieldname: "map_to_last_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
fieldtype: "Column Break",
},
{
label: __("Email Id"),
fieldname: "email_address",
fieldtype: "Data",
options: "Email",
},
{
fieldtype: "Column Break",
},
{
label: __("Mobile Number"),
fieldname: "mobile_number",

View File

@@ -115,6 +115,10 @@ erpnext.sales_common = {
company() {
super.company();
this.set_default_company_address();
if (!this.is_onload) {
// we don't want to override the mapped contact from prevdoc
this.set_default_company_contact_person();
}
}
set_default_company_address() {
@@ -139,6 +143,24 @@ erpnext.sales_common = {
}
}
set_default_company_contact_person() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_contact_person")) {
return;
}
if (this.frm.doc.company) {
frappe.db
.get_value("Company", this.frm.doc.company, "default_sales_contact")
.then((r) => {
if (r.message?.default_sales_contact) {
this.frm.set_value("company_contact_person", r.message.default_sales_contact);
} else {
this.frm.set_value("company_contact_person", "");
}
});
}
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

@@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", {
frm.set_query("customer_primary_contact", function (doc) {
return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact",
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: {
customer: doc.name,
type: "Contact",
},
};
});
frm.set_query("customer_primary_address", function (doc) {
return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: {
link_doctype: "Customer",
link_name: doc.name,
customer: doc.name,
type: "Address",
},
};
});
@@ -197,6 +200,22 @@ frappe.ui.form.on("Customer", {
var grid = cur_frm.get_field("sales_team").grid;
grid.set_column_disp("allocated_amount", false);
grid.set_column_disp("incentives", false);
frm.set_query("customer_group", () => {
return {
filters: {
is_group: 0,
},
};
});
frm.set_query("territory", () => {
return {
filters: {
is_group: 0,
},
};
});
},
validate: function (frm) {
if (frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);

View File

@@ -56,6 +56,8 @@
"customer_primary_contact",
"mobile_no",
"email_id",
"first_name",
"last_name",
"tax_tab",
"taxation_section",
"tax_id",
@@ -581,6 +583,20 @@
"no_copy": 1,
"options": "Prospect",
"print_hide": 1
},
{
"fetch_from": "customer_primary_contact.first_name",
"fieldname": "first_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "First Name"
},
{
"fetch_from": "customer_primary_contact.last_name",
"fieldname": "last_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "Last Name"
}
],
"icon": "fa fa-user",
@@ -594,7 +610,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-06-17 03:24:59.612974",
"modified": "2025-11-25 09:35:56.772949",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -672,6 +688,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "customer_group,territory, mobile_no,primary_address",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -61,12 +61,14 @@ class Customer(TransactionBase):
disabled: DF.Check
dn_required: DF.Check
email_id: DF.ReadOnly | None
first_name: DF.ReadOnly | None
gender: DF.Link | None
image: DF.AttachImage | None
industry: DF.Link | None
is_frozen: DF.Check
is_internal_customer: DF.Check
language: DF.Link | None
last_name: DF.ReadOnly | None
lead_name: DF.Link | None
loyalty_program: DF.Link | None
loyalty_program_tier: DF.Data | None
@@ -230,7 +232,7 @@ class Customer(TransactionBase):
self.update_lead_status()
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
self.link_address_and_contact()
self.copy_communication()
self.update_customer_groups()
@@ -248,7 +250,7 @@ class Customer(TransactionBase):
def create_primary_contact(self):
if not self.customer_primary_contact and not self.lead_name:
if self.mobile_no or self.email_id:
if self.mobile_no or self.email_id or self.first_name or self.last_name:
contact = make_contact(self)
self.db_set("customer_primary_contact", contact.name)
self.db_set("mobile_no", self.mobile_no)
@@ -270,15 +272,23 @@ class Customer(TransactionBase):
if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
def link_lead_address_and_contact(self):
if self.lead_name:
# assign lead address and contact to customer (if already not set)
def link_address_and_contact(self):
linked_documents = {
"Lead": self.lead_name,
"Opportunity": self.opportunity_name,
"Prospect": self.prospect_name,
}
for doctype, docname in linked_documents.items():
# assign lead, opportunity and prospect address and contact to customer (if already not set)
if not docname:
continue
linked_contacts_and_addresses = frappe.get_all(
"Dynamic Link",
filters=[
["parenttype", "in", ["Contact", "Address"]],
["link_doctype", "=", "Lead"],
["link_name", "=", self.lead_name],
["link_doctype", "=", doctype],
["link_name", "=", docname],
],
fields=["parent as name", "parenttype as doctype"],
)
@@ -736,6 +746,10 @@ def make_contact(args, is_primary_contact=1):
contact.add_email(args.get("email_id"), is_primary=True)
if args.get("mobile_no"):
contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True)
if args.get("first_name"):
contact.first_name = args.get("first_name")
if args.get("last_name"):
contact.last_name = args.get("last_name")
if flags := args.get("flags"):
contact.insert(ignore_permissions=flags.get("ignore_permissions"))
@@ -786,21 +800,29 @@ def make_address(args, is_primary_address=1, is_shipping_address=1):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_customer_primary(doctype, txt, searchfield, start, page_len, filters):
customer = filters.get("customer")
con = qb.DocType("Contact")
type = filters.get("type")
type_doctype = qb.DocType(type)
dlink = qb.DocType("Dynamic Link")
return (
qb.from_(con)
query = (
qb.from_(type_doctype)
.join(dlink)
.on(con.name == dlink.parent)
.select(con.name, con.email_id)
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
.run()
.on(type_doctype.name == dlink.parent)
.select(type_doctype.name)
.where(
(dlink.link_name == customer)
& (type_doctype.name.like(f"%{txt}%"))
& (dlink.link_doctype == "Customer")
)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
"""Parse full name into first name, middle name and last name"""

View File

@@ -252,6 +252,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
lead: this.frm.doc.party_name,
posting_date: this.frm.doc.transaction_date,
company: this.frm.doc.company,
doctype: this.frm.doc.doctype,
},
callback: function (r) {
if (r.message) {

View File

@@ -574,6 +574,10 @@ frappe.ui.form.on("Sales Order Item", {
});
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
super.setup(doc);
}
onload(doc, dt, dn) {
super.onload(doc, dt, dn);
}

View File

@@ -5,6 +5,7 @@
import json
import frappe
from frappe.query_builder import DocType, Order
from frappe.utils import cint
from frappe.utils.nestedset import get_root_of
@@ -55,7 +56,7 @@ def search_by_term(search_term, warehouse, price_list):
}
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
item_stock_qty, is_stock_item, is_negative_stock_allowed = get_stock_availability(item_code, warehouse)
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
item.update({"actual_qty": item_stock_qty})
@@ -198,20 +199,26 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
current_date = frappe.utils.today()
for item in items_data:
item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
"valid_from": ["<=", current_date],
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
)
ItemPrice = DocType("Item Price")
item_prices = (
frappe.qb.from_(ItemPrice)
.select(
ItemPrice.price_list_rate,
ItemPrice.currency,
ItemPrice.uom,
ItemPrice.batch_no,
ItemPrice.valid_from,
ItemPrice.valid_upto,
)
.where(ItemPrice.price_list == price_list)
.where(ItemPrice.item_code == item.item_code)
.where(ItemPrice.selling == 1)
.where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull()))
.where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull()))
.orderby(ItemPrice.valid_from, order=Order.desc)
).run(as_dict=True)
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
item_uom = item.stock_uom

View File

@@ -759,12 +759,16 @@ erpnext.PointOfSale.Controller = class {
const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
const available_qty = resp[0];
const is_stock_item = resp[1];
const is_negative_stock_allowed = resp[2];
frappe.dom.unfreeze();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold();
if (is_negative_stock_allowed) return;
if (!(available_qty > 0)) {
if (is_stock_item) {
frappe.model.clear_doc(item_row.doctype, item_row.name);

View File

@@ -37,6 +37,13 @@ frappe.ui.form.on("Company", {
return { filters: { selling: 1 } };
});
frm.set_query("default_sales_contact", function (doc) {
return {
query: "frappe.contacts.doctype.contact.contact.contact_query",
filters: { link_doctype: "Company", link_name: doc.name },
};
});
frm.set_query("default_buying_terms", function () {
return { filters: { buying: 1 } };
});

View File

@@ -103,6 +103,7 @@
"total_monthly_sales",
"column_break_goals",
"default_selling_terms",
"default_sales_contact",
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
@@ -851,6 +852,12 @@
"fieldtype": "Select",
"label": "Reconciliation Takes Effect On",
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
},
{
"fieldname": "default_sales_contact",
"fieldtype": "Link",
"label": "Default Sales Contact",
"options": "Contact"
}
],
"icon": "fa fa-building",
@@ -858,7 +865,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-08-25 18:34:03.602046",
"modified": "2025-11-16 16:51:27.624096",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -66,6 +66,7 @@ class Company(NestedSet):
default_payable_account: DF.Link | None
default_provisional_account: DF.Link | None
default_receivable_account: DF.Link | None
default_sales_contact: DF.Link | None
default_selling_terms: DF.Link | None
default_warehouse_for_sales_return: DF.Link | None
depreciation_cost_center: DF.Link | None

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://api.frankfurter.app/", "")
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
if test_exchange_values.get(date):
return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
@@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.assertEqual(flt(exchange_rate, 3), 65.1)
settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "frankfurter.app"
settings.service_provider = "frankfurter.dev"
settings.save()
def test_exchange_rate_strict(self, mock_get):

View File

@@ -139,7 +139,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2023-06-02 13:40:34.435822",
"modified": "2025-12-02 13:58:03.378607",
"modified_by": "Administrator",
"module": "Setup",
"name": "Customer Group",
@@ -208,9 +208,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_customer_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -10,6 +10,7 @@ from frappe.permissions import (
remove_user_permission,
)
from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils.deprecations import deprecated
from frappe.utils.nestedset import NestedSet
from erpnext.utilities.transaction_base import delete_events
@@ -88,9 +89,6 @@ class Employee(NestedSet):
if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"):
return
if not has_permission("User Permission", ptype="write", raise_exception=False):
return
employee_user_permission_exists = frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id}
)
@@ -251,8 +249,9 @@ def validate_employee_role(doc, method=None, ignore_emp_check=False):
doc.get("roles").remove(doc.get("roles", {"role": "Employee Self Service"})[0])
@deprecated
def update_user_permissions(doc, method):
# called via User hook
# formerly called via User hook
if "Employee" in [d.role for d in doc.get("roles")]:
if not has_permission("User Permission", ptype="write", raise_exception=False):
return

View File

@@ -135,7 +135,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
"modified": "2024-01-30 14:08:38.485616",
"modified": "2025-12-02 13:58:30.018021",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
@@ -202,9 +202,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_item_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -41,6 +41,7 @@
{
"bold": 1,
"default": "0",
"description": "Only leaf nodes are allowed in transaction",
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
@@ -107,7 +108,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2022-12-24 11:16:12.486719",
"modified": "2025-12-02 13:57:57.814686",
"modified_by": "Administrator",
"module": "Setup",
"name": "Supplier Group",
@@ -168,8 +169,10 @@
"share": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -46,6 +46,7 @@
{
"bold": 1,
"default": "0",
"description": "Only leaf nodes are allowed in transaction",
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
@@ -123,7 +124,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2022-12-24 11:16:39.964956",
"modified": "2025-12-02 13:58:55.190485",
"modified_by": "Administrator",
"module": "Setup",
"name": "Territory",
@@ -186,9 +187,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_territory,territory_manager",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -93,7 +93,7 @@ def setup_currency_exchange():
ces.set("result_key", [])
ces.set("req_params", [])
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}"
ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -3,8 +3,11 @@
import frappe
from frappe.defaults import get_user_default
from frappe.utils import cint
from erpnext.accounts.utils import get_fiscal_years
def boot_session(bootinfo):
"""boot session - send website info if guest"""
@@ -53,6 +56,11 @@ def boot_session(bootinfo):
)
party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""")
fiscal_year = get_fiscal_years(
frappe.utils.nowdate(), company=get_user_default("company"), boolean=True
)
if fiscal_year:
bootinfo.current_fiscal_year = fiscal_year[0]
bootinfo.party_account_types = frappe._dict(party_account_types)
bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company")

View File

@@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {

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