Compare commits

..

82 Commits

Author SHA1 Message Date
Frappe PR Bot
66b2b89bcd chore(release): Bumped to Version 15.92.0
# [15.92.0](https://github.com/frappe/erpnext/compare/v15.91.3...v15.92.0) (2025-12-16)

### Bug Fixes

* **accounts:** handle drop ship in company linked address validation ([b340d7d](b340d7d4f4))
* add link filters for item group in quickentry ([981c9c7](981c9c76c1))
* add missing query key in 'Reports To' field filter ([e1dc80b](e1dc80b6d8))
* add validation for transferred qty and handle MR transfer status for in-transit entry. (backport [#50683](https://github.com/frappe/erpnext/issues/50683)) ([#51134](https://github.com/frappe/erpnext/issues/51134)) ([6a6398a](6a6398a392))
* **currency exchange settings:** added backward compatibility for frankfurter api ([8d32ba9](8d32ba9a2e))
* delayed tasks summary chart color ([325fc61](325fc619dc))
* ensure fresh `grand_total_diff` is used for each calculation ([2d198e6](2d198e698a))
* ensure type on method parameter ([16c8b74](16c8b74d52))
* incorrect invoice qty ([ebbecdb](ebbecdba23))
* **manufacturing:** add validation for disassemble qty ([cdc0429](cdc04292f2))
* **manufacturing:** get items for disassembly order ([279cf6f](279cf6fe00))
* mark navbar item as translatable ([ec3a226](ec3a226a83))
* only show net gl balance as opening in general ledger ([0d5e45b](0d5e45bb7c))
* **payment entry:** fetch gain loss account from company boot ([c01e40d](c01e40da3c))
* precision issue on job card submission ([4ee4a57](4ee4a57f72))
* preserve user-entered exchange rates in ERR journal entries ([fa04e36](fa04e368d3))
* prevent dispatch address copying on drop ship ([5d5dff9](5d5dff9103))
* prevent self in "Reports To" dropdown (UI-level check) ([9e8bb9b](9e8bb9b235))
* putaway rule not applying on serial nos ([df820ae](df820aece6))
* re-calculate outstanding / write-off amount during submission ([5bfdc01](5bfdc010f3))
* **Rename Tool:** use "Link" field instead of "Select" ([2aff169](2aff16928c))
* **Rename Tool:** use "Link" field instead of "Select" (backport [#50995](https://github.com/frappe/erpnext/issues/50995)) ([#51138](https://github.com/frappe/erpnext/issues/51138)) ([53bb2cf](53bb2cf7c0))
* Serial/Batches not fetching when creating Material Transfer from Purchase Receipt ([f3c70a6](f3c70a66b5))
* **share balance:** use currency field instead of int for rate and amount ([a8ed281](a8ed2815a4))
* Short circuit guest perm checks ([dab8ac7](dab8ac7b1d))
* stock ageing report ([d098572](d09857294c))
* **stock:** remove total bar in chart view ([918f8ca](918f8ca79b))
* **subcontract:** ignore BOM qty validation for alternative items (backport [#51122](https://github.com/frappe/erpnext/issues/51122)) ([#51135](https://github.com/frappe/erpnext/issues/51135)) ([2c9c6c3](2c9c6c3798))
* **trial_balance:** remove hardcoded precision for currency values ([99b69c1](99b69c121e))
* use dummy translations for custom field labels ([#49875](https://github.com/frappe/erpnext/issues/49875)) ([088bbac](088bbac543))
* validate available stock with multiple dimensions (backport [#50937](https://github.com/frappe/erpnext/issues/50937)) ([#50983](https://github.com/frappe/erpnext/issues/50983)) ([98eeff8](98eeff8775))
* validate budget after cost center allocation ([a2b6e4a](a2b6e4a1c5))

### Features

* introduce extended bank transaction fields (backport [#50021](https://github.com/frappe/erpnext/issues/50021)) ([#51112](https://github.com/frappe/erpnext/issues/51112)) ([a61890e](a61890ec2b))

### Performance Improvements

* move all hourly/daily jobs to maintenance queue (backport [#47504](https://github.com/frappe/erpnext/issues/47504)) ([#51005](https://github.com/frappe/erpnext/issues/51005)) ([46ca347](46ca347578))
* sabb validate serial no ([#51132](https://github.com/frappe/erpnext/issues/51132)) ([3a9888a](3a9888aad9))

### Reverts

* changes to install_fixtures ([19dc26e](19dc26ea16))
2025-12-16 15:33:15 +00:00
ruthra kumar
d804d43ed0 Merge pull request #51124 from frappe/version-15-hotfix
chore: release v15
2025-12-16 21:01:44 +05:30
ruthra kumar
53bb2cf7c0 fix(Rename Tool): use "Link" field instead of "Select" (backport #50995) (#51138)
fix(Rename Tool): use "Link" field instead of "Select"

(cherry picked from commit ba9bbed038)

# Conflicts:
#	erpnext/utilities/doctype/rename_tool/rename_tool.json
#	erpnext/utilities/doctype/rename_tool/rename_tool.py

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-12-16 20:40:10 +05:30
Raffael Meyer
2aff16928c fix(Rename Tool): use "Link" field instead of "Select"
(cherry picked from commit ba9bbed038)

# Conflicts:
#	erpnext/utilities/doctype/rename_tool/rename_tool.json
#	erpnext/utilities/doctype/rename_tool/rename_tool.py
2025-12-16 20:23:15 +05:30
Diptanil Saha
630dcf072f Merge pull request #51144 from frappe/mergify/bp/version-15-hotfix/pr-51120
fix: add link filters for item group in quickentry (backport #51120)
2025-12-16 18:59:24 +05:30
Mihir Kandoi
b73eb47a43 Merge pull request #51140 from frappe/mergify/bp/version-15-hotfix/pr-51137 2025-12-16 18:31:52 +05:30
Afsal Syed
981c9c76c1 fix: add link filters for item group in quickentry
(cherry picked from commit 3bef6bf5ef)
2025-12-16 12:57:51 +00:00
mergify[bot]
6a6398a392 fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (backport #50683) (#51134)
fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (#50683)

* fix: add validation for transferred qty

* fix: modify if statement

* test: add unit test for mr transfer status in-transit entry

(cherry picked from commit 890316a793)

Co-authored-by: Logesh Periyasamy <logeshperiyasamy24@gmail.com>
2025-12-16 18:22:15 +05:30
Mihir Kandoi
325fc619dc fix: delayed tasks summary chart color
(cherry picked from commit 38affb0562)
2025-12-16 12:44:30 +00:00
mergify[bot]
2c9c6c3798 fix(subcontract): ignore BOM qty validation for alternative items (backport #51122) (#51135)
fix(subcontract): ignore BOM qty validation for alternative items (#51122)

(cherry picked from commit 2f19244660)

Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com>
2025-12-16 18:11:44 +05:30
rohitwaghchaure
3a9888aad9 perf: sabb validate serial no (#51132) 2025-12-16 17:37:39 +05:30
ruthra kumar
695ca39d84 Merge pull request #51129 from frappe/mergify/bp/version-15-hotfix/pr-51048
fix(payment entry): fetch gain loss account from company boot (backport #51048)
2025-12-16 17:00:49 +05:30
ruthra kumar
9032d4c3d6 Merge pull request #51110 from one-highflyer/fix/err-preserve-exchange-rate
fix: preserve user-entered exchange rates in ERR journal entries
2025-12-16 16:47:12 +05:30
ravibharathi656
c01e40da3c fix(payment entry): fetch gain loss account from company boot
(cherry picked from commit 8e54be7808)
2025-12-16 10:58:21 +00:00
ruthra kumar
552cb5c528 Merge pull request #51127 from frappe/mergify/bp/version-15-hotfix/pr-51123
fix: ensure type on method parameter (backport #51123)
2025-12-16 15:59:49 +05:30
ruthra kumar
e6ccb00d4c Merge pull request #50539 from frappe/mergify/bp/version-15-hotfix/pr-49875
fix: use dummy translations for custom field labels (backport #49875)
2025-12-16 15:54:27 +05:30
ruthra kumar
5b481d9235 Merge pull request #51071 from frappe/mergify/bp/version-15-hotfix/pr-51041
fix(trial_balance): remove hardcoded precision for currency values (backport #51041)
2025-12-16 15:40:02 +05:30
ruthra kumar
6b27b659e3 Merge pull request #51017 from frappe/mergify/bp/version-15-hotfix/pr-50948
fix(stock): remove total bar in chart view (backport #50948)
2025-12-16 15:36:04 +05:30
ruthra kumar
16c8b74d52 fix: ensure type on method parameter
(cherry picked from commit c055e86e51)
2025-12-16 10:03:53 +00:00
Mihir Kandoi
8dcb2f39c2 Merge pull request #51088 from aerele/v15-drop-ship-retain-address 2025-12-16 13:28:02 +05:30
ravibharathi656
5d5dff9103 fix: prevent dispatch address copying on drop ship 2025-12-16 13:10:38 +05:30
ruthra kumar
b529a6d00c Merge pull request #51116 from frappe/mergify/bp/version-15-hotfix/pr-51077
refactor: standardize cost_center updation across transactions (backport #51077)
2025-12-16 11:07:51 +05:30
Navin-S-R
41659a875b refactor: standardize cost_center updation across transactions
(cherry picked from commit c28f6f1856)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.js
2025-12-16 11:04:41 +05:30
mergify[bot]
a61890ec2b feat: introduce extended bank transaction fields (backport #50021) (#51112)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
Co-authored-by: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
2025-12-15 23:04:16 +01:00
Imesha Sudasingha
fa04e368d3 fix: preserve user-entered exchange rates in ERR journal entries
The JE creation was overriding exchange_rate=1 with the system rate.
Set ignore_exchange_rate flag to preserve user values.
2025-12-15 20:23:06 +05:30
Venkatesh
98eeff8775 fix: validate available stock with multiple dimensions (backport #50937) (#50983)
* fix: validate available stock with multiple dimensions

* test: validate negative stock with multiple inventory dimensions

* chore: reset document_wise_inventory_dimensions
2025-12-15 19:07:06 +05:30
Smit Vora
64acf179db Merge pull request #51105 from frappe/mergify/bp/version-15-hotfix/pr-50782
fix: only show net balance as opening in general ledger (backport #50782)
2025-12-15 15:50:49 +05:30
Smit Vora
0d5e45bb7c fix: only show net gl balance as opening in general ledger
(cherry picked from commit b7c7e0746e)
2025-12-15 10:00:49 +00:00
Mihir Kandoi
fc86784eb1 Merge pull request #51096 from frappe/mergify/bp/version-15-hotfix/pr-49139 2025-12-15 03:50:07 +05:30
Anjali Patel
e1dc80b6d8 fix: add missing query key in 'Reports To' field filter
(cherry picked from commit cbfb14a654)
2025-12-14 20:26:21 +00:00
Anjali Patel
9e8bb9b235 fix: prevent self in "Reports To" dropdown (UI-level check)
Ensures employee cannot select themselves in the "Reports To" field via UI.
This complements server-side validation by improving UX.

(cherry picked from commit 608d38a172)
2025-12-14 20:26:20 +00:00
mergify[bot]
ae90ee3f17 Merge pull request #51087 from frappe/mergify/bp/version-15-hotfix/pr-51063
fix(transaction-deletion): Add virtual doctypes to the list of ignored doctypes (backport #51063)
2025-12-14 14:30:11 +05:30
Ankush Menat
dab8ac7b1d fix: Short circuit guest perm checks 2025-12-14 12:11:55 +05:30
rohitwaghchaure
ce769d3a2f Merge pull request #51082 from frappe/mergify/bp/version-15-hotfix/pr-51079
fix: stock ageing report (backport #51079)
2025-12-13 07:39:04 +05:30
Rohit Waghchaure
d09857294c fix: stock ageing report
(cherry picked from commit cb84ffd972)
2025-12-12 15:13:41 +00:00
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
Khushi Rawat
030ce6d6a0 Merge pull request #51074 from frappe/mergify/bp/version-15-hotfix/pr-51070
fix: validate budget after cost center allocation (backport #51070)
2025-12-12 14:15:45 +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
rohitwaghchaure
97b253740b Merge pull request #51072 from frappe/mergify/bp/version-15-hotfix/pr-51034
fix(accounts): handle drop ship in company linked address validation (backport #51034)
2025-12-12 14:07:15 +05:30
rohitwaghchaure
94c3d66f2f Merge pull request #51073 from frappe/mergify/bp/version-15-hotfix/pr-51069
fix: incorrect invoice qty (backport #51069)
2025-12-12 13:58:24 +05:30
khushi8112
a2b6e4a1c5 fix: validate budget after cost center allocation
(cherry picked from commit f9be8a46fb)
2025-12-12 08:22:40 +00:00
Rohit Waghchaure
ebbecdba23 fix: incorrect invoice qty
(cherry picked from commit 96cdb7d54f)
2025-12-12 08:07:11 +00:00
Sudharsanan11
2263f9a477 test(accounts): add validation test for dispatch address with drop ship enabled
(cherry picked from commit f6a96e5563)
2025-12-12 08:05:59 +00:00
Sudharsanan11
b340d7d4f4 fix(accounts): handle drop ship in company linked address validation
(cherry picked from commit 2ec119e561)
2025-12-12 08:05:58 +00:00
Navin-S-R
99b69c121e fix(trial_balance): remove hardcoded precision for currency values
(cherry picked from commit a8af04f6fc)
2025-12-12 07:40:41 +00:00
rohitwaghchaure
860486bb34 Merge pull request #51068 from frappe/mergify/bp/version-15-hotfix/pr-51047
fix(manufacturing): get items for disassembly order (backport #51047)
2025-12-12 13:10:36 +05:30
rohitwaghchaure
1693e3ef3f chore: fix conflicts 2025-12-12 11:53:18 +05:30
Sudharsanan11
279cf6fe00 fix(manufacturing): get items for disassembly order
(cherry picked from commit 99148a2aba)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.py
2025-12-12 06:15:12 +00:00
Sudharsanan11
cdc04292f2 fix(manufacturing): add validation for disassemble qty
(cherry picked from commit 86d6facab3)
2025-12-12 06:15:11 +00:00
Sagar Vora
6788b58d1c Merge pull request #51059 from frappe/mergify/bp/version-15-hotfix/pr-51057
fix: re-calculate outstanding / write-off amount during submission (backport #51057)
2025-12-11 23:26:06 +05:30
Sagar Vora
5bfdc010f3 fix: re-calculate outstanding / write-off amount during submission
(cherry picked from commit 09c9ac1b66)
2025-12-11 17:35:25 +00:00
Sagar Vora
0e7efd75cd Merge pull request #51053 from frappe/mergify/bp/version-15-hotfix/pr-51051
fix: ensure fresh `grand_total_diff` is used for each calculation (backport #51051)
2025-12-11 18:07:37 +05:30
Sagar Vora
2d198e698a fix: ensure fresh grand_total_diff is used for each calculation
(cherry picked from commit b3fdef8d19)
2025-12-11 12:35:29 +00:00
Diptanil Saha
857ab70f4e Merge pull request #51045 from frappe/mergify/bp/version-15-hotfix/pr-51037
fix(currency exchange settings): added backward compatibility for frankfurter api (backport #51037)
2025-12-11 16:28:03 +05:30
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
Diptanil Saha
113da4f512 chore: resolve conflict 2025-12-11 15:59:57 +05:30
diptanilsaha
8d32ba9a2e fix(currency exchange settings): added backward compatibility for frankfurter api
(cherry picked from commit 5c2bb66028)

# Conflicts:
#	erpnext/patches.txt
2025-12-11 10:26:45 +00:00
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
rohitwaghchaure
8f569d9711 Merge pull request #51036 from frappe/mergify/bp/version-15-hotfix/pr-51035
fix: put-away rule not applying on serial nos (backport #51035)
2025-12-11 15:43:51 +05:30
Rohit Waghchaure
df820aece6 fix: putaway rule not applying on serial nos
(cherry picked from commit 6bb0bdcdca)
2025-12-11 09:10:12 +00:00
rohitwaghchaure
6530cfe84b Merge pull request #51029 from frappe/mergify/bp/version-15-hotfix/pr-51027
fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt (backport #51027)
2025-12-11 13:14:46 +05:30
rohitwaghchaure
c8565c47a2 chore: fix conflicts 2025-12-11 11:36:31 +05:30
Rohit Waghchaure
f3c70a66b5 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
2025-12-11 05:29:24 +00:00
barredterra
115fd48bbf Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875 2025-12-10 13:28:30 +01:00
Sudharsanan11
918f8ca79b fix(stock): remove total bar in chart view
(cherry picked from commit 198eb372e3)
2025-12-10 09:42:17 +00:00
mergify[bot]
46ca347578 perf: move all hourly/daily jobs to maintenance queue (backport #47504) (#51005)
perf: move all hourly/daily jobs to maintenance queue (#47504)

None of them need to strictly happen at 00:00 or *:00, so moving them all to maintenance queue which executes with same frequency but spaced out.

(cherry picked from commit a50251401f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-12-10 06:19:50 +00:00
Diptanil Saha
8226502956 Merge pull request #51003 from frappe/mergify/bp/version-15-hotfix/pr-51001
fix(share balance): use currency field instead of int for rate and amount (backport #51001)
2025-12-10 10:38:42 +05:30
Mihir Kandoi
71e537b030 Merge pull request #51004 from frappe/mergify/bp/version-15-hotfix/pr-50952
fix: precision issue on job card submission (backport #50952)
2025-12-10 10:08:00 +05:30
Diptanil Saha
8fd3e8e22e chore: resolve conflict 2025-12-10 09:59:04 +05:30
Dany Robert
4ee4a57f72 fix: precision issue on job card submission
(cherry picked from commit 80730908c9)
2025-12-10 04:22:20 +00:00
diptanilsaha
a8ed2815a4 fix(share balance): use currency field instead of int for rate and amount
(cherry picked from commit 2fe5fad884)

# Conflicts:
#	erpnext/accounts/doctype/share_balance/share_balance.json
2025-12-10 04:21:07 +00:00
barredterra
2c13c4746b Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875 2025-11-21 00:46:43 +01:00
barredterra
ec3a226a83 fix: mark navbar item as translatable 2025-11-15 19:55:59 +01:00
barredterra
19dc26ea16 revert: changes to install_fixtures
I think this would be too breaking. Custom apps might expect the translated data to exist.
2025-11-15 19:51:05 +01:00
barredterra
e29a384f90 chore: resolve conflicts 2025-11-15 19:39:40 +01:00
Raffael Meyer
088bbac543 fix: use dummy translations for custom field labels (#49875)
(cherry picked from commit 9a989a84fb)

# Conflicts:
#	erpnext/setup/install.py
#	erpnext/setup/setup_wizard/operations/install_fixtures.py
2025-11-15 18:34:57 +00:00
49 changed files with 661 additions and 201 deletions

View File

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

View File

@@ -38,7 +38,10 @@
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
"bank_party_iban",
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
],
"fields": [
{
@@ -233,12 +236,32 @@
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-10-23 17:32:58.514807",
"modified": "2025-12-07 20:49:18.600757",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -32,6 +32,8 @@ class BankTransaction(Document):
date: DF.Date | None
deposit: DF.Currency
description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None
party_type: DF.Link | None
@@ -45,9 +47,11 @@ class BankTransaction(Document):
# end: auto-generated types
def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount()
def validate(self):
self.validate_included_fee()
self.validate_duplicate_references()
self.validate_currency()
@@ -307,6 +311,40 @@ class BankTransaction(Document):
self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():

View File

@@ -0,0 +1,133 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
class TestBankTransactionFees(FrappeTestCase):
def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 101
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
def test_included_fee_allows_equal(self):
"""A fee that's part of a withdrawal may be equal to the withdrawal
amount (only the fee was deducted from the account)."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 100
bt.validate_included_fee()
def test_included_fee_allows_for_deposit(self):
"""For deposits, a fee may be recorded separately without limiting the
received amount."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.included_fee = 999
bt.validate_included_fee()
def test_excluded_fee_noop_when_zero(self):
"""When there is no excluded fee to apply, the amounts should remain
unchanged."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 5
bt.excluded_fee = 0
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 100)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_throws_when_exceeds_deposit(self):
"""A fee deducted from an incoming payment must not exceed the incoming
amount (else it would be a withdrawal, a conversion we don't support)."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.excluded_fee = 11
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
"""A transaction must be either incoming or outgoing when applying a
fee, not both."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.withdrawal = 10
bt.excluded_fee = 1
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_deducts_from_deposit(self):
"""When a fee is deducted from an incoming payment, the net received
amount decreases and the fee is tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 95)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
"""A separately-deducted fee may reduce an incoming payment to zero,
while still tracking the fee."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 5
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_increases_outgoing_payment(self):
"""When a separately-deducted fee is provided for an outgoing payment,
the total money leaving increases and the fee is tracked."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 100
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 105)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
"""If only an excluded fee is provided, it should be treated as an
outgoing payment and the fee is then tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 5)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)

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.dev") {
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -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.dev":
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
self.set("result_key", [])
self.set("req_params", [])
@@ -105,9 +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.dev"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
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}"

View File

@@ -486,6 +486,9 @@ class ExchangeRateRevaluation(Document):
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
# Prevent JE from overriding user-entered exchange rates (e.g., rate of 1)
journal_entry.flags.ignore_exchange_rate = True
journal_entry_accounts = []
for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):

View File

@@ -1302,15 +1302,14 @@ frappe.ui.form.on("Payment Entry", {
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const response = await get_company_defaults(frm.doc.company);
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
const account =
response.message?.[account_fieldname] ||
company_defaults?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = response.message?.cost_center;
row.cost_center = company_defaults?.cost_center;
row.is_exchange_gain_loss = 1;
}

View File

@@ -545,6 +545,9 @@ def make_payment_request(**args):
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company

View File

@@ -575,17 +575,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.cost_center) {
var cl = doc.items || [];
for (var i = 0; i < cl.length; i++) {
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
}
}
refresh_field("items");
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],

View File

@@ -648,10 +648,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
};
cur_frm.set_query("debit_to", function (doc) {
return {
filters: {

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-10 18:32:36.201124",
"modified": "2025-12-10 08:06:40.611761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Int
amount: DF.Currency
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Int
rate: DF.Currency
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -199,19 +199,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
for d in gl_map:
cost_center = d.get("cost_center")
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
# Validate budget against main cost center
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
@@ -414,7 +415,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.notify_update = False
gle.submit()
if not from_repost and gle.voucher_type != "Period Closing Voucher":
if (
not from_repost
and gle.voucher_type != "Period Closing Voucher"
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
):
validate_expense_against_budget(args)

View File

@@ -482,7 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle):
def update_value_in_dict(data, key, gle, show_net_values=False):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -493,10 +493,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -526,11 +530,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -399,7 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
}
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
row[key] = flt(d.get(key, 0.0))
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values

View File

@@ -312,13 +312,30 @@ class AccountsController(TransactionBase):
def validate_company_linked_addresses(self):
address_fields = []
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
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 Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
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",
{
@@ -329,11 +346,15 @@ class AccountsController(TransactionBase):
},
):
frappe.throw(
_("{0} does not belong to the {1}.").format(
_("{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:
self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")

View File

@@ -378,6 +378,9 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -587,7 +590,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
grand_total_diff = self.grand_total_diff
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -850,12 +853,11 @@ class calculate_taxes_and_totals:
)
)
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

View File

@@ -2434,6 +2434,7 @@ class TestAccountsController(FrappeTestCase):
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"
@@ -2462,3 +2463,16 @@ class TestAccountsController(FrappeTestCase):
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

@@ -412,29 +412,29 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
],
# Daily but offset by 45 minutes
"45 0 * * *": [
"erpnext.stock.reorder_item.reorder_item",
],
"45 0 * * *": [],
},
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
"hourly_long": [],
"hourly_maintenance": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"daily": [
"daily": [],
"daily_long": [],
"daily_maintenance": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
@@ -458,17 +458,16 @@ scheduler_events = {
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
"erpnext.stock.reorder_item.reorder_item",
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",

View File

@@ -403,6 +403,10 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.then((data) => {
if (flt(data.qty) <= 0) {
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
return;
}
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: "Disassemble",

View File

@@ -979,14 +979,14 @@ class WorkOrder(Document):
for d in self.get("operations"):
precision = d.precision("completed_qty")
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision)
if not qty:
d.status = "Pending"
elif flt(qty) < flt(self.qty):
elif qty < flt(self.qty, precision):
d.status = "Work in Progress"
elif flt(qty) == flt(self.qty):
elif qty == flt(self.qty, precision):
d.status = "Completed"
elif flt(qty) <= max_allowed_qty_for_wo:
elif qty <= flt(max_allowed_qty_for_wo, precision):
d.status = "Completed"
else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
@@ -1509,7 +1509,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items(qty, work_order.production_item)
stock_entry.get_items()
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()

View File

@@ -426,4 +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
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11

View File

@@ -2,8 +2,13 @@ import frappe
def execute():
settings_meta = frappe.get_meta("Currency Exchange Settings")
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
if (
"frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n")
or settings.service_provider != "frankfurter.app"
):
return
settings.service_provider = "frankfurter.dev"

View File

@@ -75,13 +75,27 @@ def get_chart_data(data):
delay = delay + 1
else:
on_track = on_track + 1
labels = []
datasets = []
colors = []
if on_track:
labels.append(_("On Track"))
datasets.append(on_track)
colors.append("#84D5BA")
if delay:
labels.append(_("Delayed"))
datasets.append(delay)
colors.append("#CB4B5F")
charts = {
"data": {
"labels": [_("On Track"), _("Delayed")],
"datasets": [{"name": _("Delayed"), "values": [on_track, delay]}],
"labels": labels,
"datasets": [{"name": _("Delayed"), "values": datasets}],
},
"type": "percentage",
"colors": ["#84D5BA", "#CB4B5F"],
"colors": colors,
}
return charts

View File

@@ -343,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
// reset value from earlier calculations
this.grand_total_diff = 0;
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
@@ -578,6 +581,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.grand_total_diff = diff;
} else {
me.grand_total_diff = 0;
}
}
}
@@ -587,7 +592,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
const grand_total_diff = this.grand_total_diff;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff

View File

@@ -1093,12 +1093,8 @@ 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");
cost_center(doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
}
due_date(doc, cdt, cdn) {

View File

@@ -1408,7 +1408,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1417,6 +1416,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},
@@ -1549,7 +1549,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1558,6 +1557,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},

View File

@@ -11,7 +11,13 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
};
};
this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.employee_query" };
return {
query: "erpnext.controllers.queries.employee_query",
filters: [
["status", "=", "Active"],
["name", "!=", doc.name],
],
};
};
}

View File

@@ -347,8 +347,9 @@ class TransactionDeletionRecord(Document):
self.db_set("error_log", None)
def get_doctypes_to_be_ignored_list(self):
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
doctypes_to_be_ignored_list = singles
doctypes_to_be_ignored_list = frappe.get_all(
"DocType", or_filters=[["issingle", "=", 1], ["is_virtual", "=", 1]], pluck="name"
)
for doctype in self.doctypes_to_be_ignored:
doctypes_to_be_ignored_list.append(doctype.doctype_name)

View File

@@ -4,14 +4,13 @@
import click
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint
import erpnext
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from erpnext.setup.utils import identity as _
from .default_success_action import get_default_success_action
@@ -193,28 +192,27 @@ def add_company_to_session_defaults():
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
erpnext_navbar_items = [
{
"item_label": "Documentation",
"item_label": _("Documentation"),
"item_type": "Route",
"route": "https://docs.erpnext.com/",
"is_standard": 1,
},
{
"item_label": "User Forum",
"item_label": _("User Forum"),
"item_type": "Route",
"route": "https://discuss.frappe.io",
"is_standard": 1,
},
{
"item_label": "Frappe School",
"item_label": _("Frappe School"),
"item_type": "Route",
"route": "https://frappe.io/school?utm_source=in_app",
"is_standard": 1,
},
{
"item_label": "Report an Issue",
"item_label": _("Report an Issue"),
"item_type": "Route",
"route": "https://github.com/frappe/erpnext/issues",
"is_standard": 1,

View File

@@ -234,3 +234,15 @@ def welcome_email():
site_name = get_default_company() or "ERPNext"
title = _("Welcome to {0}").format(site_name)
return title
def identity(x, *args, **kwargs):
"""Used for redefining the translation function to return the string as is.
We want to create english records but still mark the strings as translatable.
E.g. when the respective DocTypes have 'Translate Link Fields' enabled or
we're creating custom fields.
Use like this: `from erpnext.setup.utils import identity as _`
"""
return x

View File

@@ -50,7 +50,7 @@ def boot_session(bootinfo):
bootinfo.docs += frappe.db.sql(
"""select name, default_currency, cost_center, default_selling_terms, default_buying_terms,
default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""",
default_letter_head, default_bank_account, enable_perpetual_inventory, country, exchange_gain_loss_account from `tabCompany`""",
as_dict=1,
update={"doctype": ":Company"},
)

View File

@@ -130,10 +130,6 @@ frappe.ui.form.on("Delivery Note Item", {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "expense_account", d.expense_account);
},
cost_center: function (frm, dt, dn) {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "cost_center", d.cost_center);
},
});
erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (

View File

@@ -504,6 +504,61 @@ class TestInventoryDimension(FrappeTestCase):
self.assertEqual(site_name, "Site 1")
def test_validate_negative_stock_with_multiple_dimension(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
item_code = "Test Negative Multi Inventory Dimension Item"
create_item(item_code)
inv_dimension_1 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Inv Site",
reference_document="Inv Site",
document_type="Inv Site",
validate_negative_stock=1,
)
inv_dimension_1.db_set("validate_negative_stock", 1)
inv_dimension_2 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Rack",
reference_document="Rack",
document_type="Rack",
validate_negative_stock=1,
)
inv_dimension_2.db_set("validate_negative_stock", 1)
frappe.local.inventory_dimensions = {}
frappe.local.document_wise_inventory_dimensions = {}
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True)
dn_doc.items[0].inv_site = "Site 2"
dn_doc.items[0].rack = "Rack 1"
dn_doc.save()
self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit)
def get_voucher_sl_entries(voucher_no, fields):
return frappe.get_all(
@@ -593,7 +648,7 @@ def prepare_test_data():
}
).insert(ignore_permissions=True)
for rack in ["Rack 1"]:
for rack in ["Rack 1", "Rack 2"]:
if not frappe.db.exists("Rack", rack):
frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True)

View File

@@ -167,6 +167,7 @@
"in_preview": 1,
"in_standard_filter": 1,
"label": "Item Group",
"link_filters": "[[\"Item Group\",\"is_group\",\"=\",0]]",
"oldfieldname": "item_group",
"oldfieldtype": "Link",
"options": "Item Group",
@@ -894,7 +895,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-12-04 09:11:56.029567",
"modified": "2025-12-15 20:08:35.634046",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -19,6 +19,7 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -926,6 +927,49 @@ class TestMaterialRequest(FrappeTestCase):
pl_for_pending = create_pick_list(mr.name)
self.assertEqual(pl_for_pending.locations[0].qty, 5)
def test_mr_status_with_partial_and_excess_end_transit(self):
material_request = make_material_request(
material_request_type="Material Transfer",
item_code="_Test Item Home Desktop 100",
)
in_transit_wh = get_in_transit_warehouse(material_request.company)
# Make sure stock is available in source warehouse
self._insert_stock_entry(20.0, 20.0)
# Stock Entry (Transfer to In-Transit)
stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_1.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_1.save().submit()
stock_entry_2 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_2.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_2.save().submit()
end_transit_1 = make_stock_in_entry(stock_entry_1.name)
end_transit_1.save().submit()
# Material Request Transfer Status should still be In Transit
material_request.load_from_db()
self.assertEqual(material_request.transfer_status, "In Transit")
end_transit_2 = make_stock_in_entry(stock_entry_2.name)
end_transit_2.items[0].update({"qty": 6}) # More than transferred
end_transit_2.save()
self.assertRaises(frappe.ValidationError, end_transit_2.submit)
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import CombineDatetime
from frappe.query_builder.functions import Abs, CombineDatetime, Sum
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
from pypika import functions as fn
@@ -19,6 +19,11 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation,
get_batches_from_bundle,
get_serial_nos_from_bundle,
)
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -1367,21 +1372,26 @@ def get_invoiced_qty_map(purchase_receipt):
def get_returned_qty_map(purchase_receipt):
"""returns a map: {so_detail: returned_qty}"""
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent
and pr.docstatus = 1
and pr.is_return = 1
and pr.return_against = %s
""",
purchase_receipt,
)
)
"""returns a map: {pr_detail: returned_qty}"""
return returned_qty_map
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr.name == pr_item.parent)
.select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty"))
.where(
(pr.docstatus == 1)
& (pr.is_return == 1)
& (pr.return_against == purchase_receipt)
& (pr_item.purchase_receipt_item.isnotnull())
)
.groupby(pr_item.purchase_receipt_item)
).run(as_list=1)
return frappe._dict(query) if query else frappe._dict()
@frappe.whitelist()
@@ -1411,6 +1421,35 @@ def make_stock_entry(source_name, target_doc=None):
target.purpose = "Material Transfer"
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
if source_doc.serial_and_batch_bundle:
serial_nos = get_serial_nos_from_bundle(source_doc.serial_and_batch_bundle)
if serial_nos:
serial_nos = "\n".join(serial_nos)
batches = get_batches_from_bundle(source_doc.serial_and_batch_bundle)
if batches:
if len(batches) == 1:
target_doc.use_serial_batch_fields = 1
target_doc.batch_no = next(iter(batches))
elif not serial_nos:
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Outward",
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
"item_code": source_doc.item_code,
"warehouse": source_doc.warehouse,
}
)
cls_obj.duplicate_package()
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if serial_nos:
target_doc.use_serial_batch_fields = 1
target_doc.serial_no = serial_nos
doclist = get_mapped_doc(
"Purchase Receipt",
source_name,
@@ -1425,6 +1464,7 @@ def make_stock_entry(source_name, target_doc=None):
"parent": "reference_purchase_receipt",
"batch_no": "batch_no",
},
"postprocess": update_item,
},
},
target_doc,

View File

@@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
@@ -119,13 +120,19 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item = frappe._dict(item)
source_warehouse = item.get("s_warehouse")
serial_nos = []
if item.get("serial_no"):
serial_nos = get_serial_nos(item.get("serial_no"))
item.conversion_factor = flt(item.conversion_factor) or 1.0
pending_qty, item_code = flt(item.qty), item.item_code
pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number")
if not pending_qty or not item_code:
updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table)
updated_table = add_row(
item, pending_qty, source_warehouse or item.warehouse, updated_table, serial_nos=serial_nos
)
continue
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
@@ -141,7 +148,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
# rules available, but no free space
items_not_accomodated.append([item_code, pending_qty])
else:
updated_table = add_row(item, pending_qty, warehouse, updated_table)
updated_table = add_row(item, pending_qty, warehouse, updated_table, serial_nos=serial_nos)
continue
# maintain item/item-warehouse wise rules, to handle if item is entered twice
@@ -167,7 +174,9 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if not qty_to_allocate:
break
updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name)
updated_table = add_row(
item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
)
pending_stock_qty -= stock_qty_to_allocate
pending_qty -= qty_to_allocate
@@ -265,7 +274,7 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
return False, vacant_rules
def add_row(item, to_allocate, warehouse, updated_table, rule=None):
def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None):
new_updated_table_row = copy.deepcopy(item)
new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
new_updated_table_row.name = None
@@ -283,6 +292,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None):
if rule:
new_updated_table_row.putaway_rule = rule
if serial_nos:
new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate)
new_updated_table_row.serial_and_batch_bundle = ""
updated_table.append(new_updated_table_row)
@@ -311,3 +323,12 @@ def show_unassigned_items_message(items_not_accomodated):
""".format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
def get_serial_nos_to_allocate(serial_nos, to_allocate):
if serial_nos:
allocated_serial_nos = serial_nos[0 : cint(to_allocate)]
serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list
return "\n".join(allocated_serial_nos) if allocated_serial_nos else ""
else:
return ""

View File

@@ -2008,6 +2008,8 @@ def get_available_serial_nos(kwargs):
filters["name"] = ("in", time_based_serial_nos)
elif ignore_serial_nos:
filters["name"] = ("not in", ignore_serial_nos)
elif kwargs.get("serial_nos"):
filters["name"] = ("in", kwargs.get("serial_nos"))
if kwargs.get("batches"):
batches = get_non_expired_batches(kwargs.get("batches"))

View File

@@ -8,6 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
@@ -1905,7 +1906,7 @@ class StockEntry(StockController):
},
)
def get_items_for_disassembly(self, disassemble_qty, production_item):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
@@ -1918,7 +1919,7 @@ class StockEntry(StockController):
items_dict = get_bom_items_as_dict(
self.bom_no,
self.company,
disassemble_qty,
self.fg_completed_qty,
fetch_exploded=self.use_multi_level_bom,
fetch_qty_in_stock_uom=False,
)
@@ -1935,8 +1936,8 @@ class StockEntry(StockController):
child_row.qty = bom_items.get("qty", child_row.qty)
child_row.amount = bom_items.get("amount", child_row.amount)
if row.item_code == production_item:
child_row.qty = disassemble_qty
if row.is_finished_item:
child_row.qty = self.fg_completed_qty
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = row.s_warehouse
@@ -1972,12 +1973,12 @@ class StockEntry(StockController):
)
@frappe.whitelist()
def get_items(self, qty=None, production_item=None):
def get_items(self):
self.set("items", [])
self.validate_work_order()
if self.purpose == "Disassemble" and qty is not None:
return self.get_items_for_disassembly(qty, production_item)
if self.purpose == "Disassemble":
return self.get_items_for_disassembly()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))
@@ -2815,6 +2816,17 @@ class StockEntry(StockController):
},
)
if d.docstatus == 1:
transfer_qty = frappe.get_value("Stock Entry Detail", d.ste_detail, "transfer_qty")
if transferred_qty and transferred_qty[0]:
if transferred_qty[0].qty > transfer_qty:
frappe.throw(
_(
"Row {0}: Transferred quantity cannot be greater than the requested quantity."
).format(d.idx)
)
stock_entries[(d.against_stock_entry, d.ste_detail)] = (
transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0
) or 0.0
@@ -2878,7 +2890,7 @@ class StockEntry(StockController):
parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit")
for item in self.items:
material_request = item.material_request or None
material_request = item.get("material_request")
if self.purpose == "Material Transfer" and material_request not in material_requests:
if self.outgoing_stock_entry and parent_se:
material_request = frappe.get_value(
@@ -2887,6 +2899,11 @@ class StockEntry(StockController):
if material_request and material_request not in material_requests:
material_requests.append(material_request)
if status == "Completed":
qty = get_transferred_qty(material_request)
if qty.get("transfer_qty") > qty.get("transferred_qty"):
status = "In Transit"
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
def set_serial_no_batch_for_finished_good(self):
@@ -3545,3 +3562,18 @@ def get_batchwise_serial_nos(item_code, row):
batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos])
return batchwise_serial_nos
def get_transferred_qty(material_request):
sed = DocType("Stock Entry Detail")
query = (
frappe.qb.from_(sed)
.select(
Sum(sed.transfer_qty).as_("transfer_qty"),
Sum(sed.transferred_qty).as_("transferred_qty"),
)
.where((sed.material_request == material_request) & (sed.docstatus == 1))
).run(as_dict=True)
return query[0]

View File

@@ -113,17 +113,15 @@ class StockLedgerEntry(Document):
return
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value)
available_qty = self.get_available_qty_after_prev_transaction(dimensions)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimension, dimension_value)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
def get_available_qty_after_prev_transaction(self, dimension, dimension_value):
def get_available_qty_after_prev_transaction(self, dimensions):
sle = frappe.qb.DocType("Stock Ledger Entry")
available_qty = (
available_qty_query = (
frappe.qb.from_(sle)
.select(Sum(sle.actual_qty))
.where(
@@ -132,21 +130,27 @@ class StockLedgerEntry(Document):
& (sle.posting_datetime < self.posting_datetime)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle[dimension] == dimension_value)
)
).run()
)
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty_query = available_qty_query.where(sle[dimension] == dimension_value)
available_qty = available_qty_query.run()
return available_qty[0][0] or 0
def throw_validation_error(self, diff, dimension, dimension_value):
def throw_validation_error(self, diff, dimensions):
msg = _(
"{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction."
"{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction."
).format(
abs(diff),
frappe.get_desk_link("Item", self.item_code),
frappe.get_desk_link("Warehouse", self.warehouse),
frappe.bold(dimension),
frappe.bold(dimension_value),
frappe.bold(
", ".join([f"{dimension}: {values.get('value')}" for dimension, values in dimensions.items()])
),
self.posting_date,
self.posting_time,
frappe.get_desk_link(self.voucher_type, self.voucher_no),

View File

@@ -20,6 +20,9 @@ def execute(filters=None):
def get_chart_data(data, filters):
def wrap_in_quotes(label):
return f"'{label}'"
if not data:
return []
@@ -36,6 +39,9 @@ def get_chart_data(data, filters):
data = data[:10]
for row in data:
if row[0] == wrap_in_quotes(_("Total")):
continue
labels.append(row[0])
datapoints.append(row[-1])

View File

@@ -273,6 +273,7 @@ class FIFOSlots:
else:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or []
serial_nos = self.uppercase_serial_nos(serial_nos)
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
@@ -289,6 +290,10 @@ class FIFOSlots:
return self.item_details
def uppercase_serial_nos(self, serial_nos):
"Convert serial nos to uppercase for uniformity."
return [sn.upper() for sn in serial_nos]
def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."

View File

@@ -543,6 +543,12 @@ class SubcontractingReceipt(SubcontractingController):
for row in self.items:
precision = row.precision("qty")
# if allow alternative item, ignore the validation as per BOM required qty
is_allow_alternative_item = frappe.db.get_value("BOM", row.bom, "allow_alternative_item")
if is_allow_alternative_item:
continue
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):

View File

@@ -199,7 +199,7 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
customer = contact_doc.get_link_for("Customer")
ignore_permissions = False
if is_website_user():
if is_website_user() and user != "Guest":
if not filters:
filters = {}

View File

@@ -2,14 +2,6 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Rename Tool", {
onload: function (frm) {
return frappe.call({
method: "erpnext.utilities.doctype.rename_tool.rename_tool.get_doctypes",
callback: function (r) {
frm.set_df_property("select_doctype", "options", r.message);
},
});
},
refresh: function (frm) {
frm.disable_save();

View File

@@ -8,27 +8,13 @@
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Select DocType",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Link",
"label": "Select DocType",
"link_filters": "[[\"DocType\",\"allow_rename\",\"=\",1],[\"DocType\",\"module\",\"!=\",\"Core\"]]",
"options": "DocType"
},
{
"allow_on_submit": 0,
"bold": 0,
@@ -72,22 +58,18 @@
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 1,
"modified": "2015-10-19 03:04:49.097140",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"issingle": 1,
"links": [],
"max_attachments": 1,
"modified": "2025-12-09 14:18:33.838623",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
@@ -112,4 +94,4 @@
],
"read_only": 0,
"read_only_onload": 0
}
}

View File

@@ -7,6 +7,7 @@
import frappe
from frappe.model.document import Document
from frappe.model.rename_doc import bulk_rename
from frappe.utils.deprecations import deprecated
class RenameTool(Document):
@@ -19,13 +20,14 @@ class RenameTool(Document):
from frappe.types import DF
file_to_rename: DF.Attach | None
select_doctype: DF.Literal
select_doctype: DF.Link | None
# end: auto-generated types
pass
@frappe.whitelist()
@deprecated
def get_doctypes():
return frappe.db.sql_list(
"""select name from tabDocType