Compare commits

..

322 Commits

Author SHA1 Message Date
Frappe PR Bot
cf337824e7 chore(release): Bumped to Version 15.108.2
## [15.108.2](https://github.com/frappe/erpnext/compare/v15.108.1...v15.108.2) (2026-05-20)

### Bug Fixes

* add warehouse vaildation for repack entry (backport [#54866](https://github.com/frappe/erpnext/issues/54866)) ([#54900](https://github.com/frappe/erpnext/issues/54900)) ([b969662](b969662b6c))
* item leaderboard uses Sales/Purchase Invoice instead of Orders ([#55038](https://github.com/frappe/erpnext/issues/55038)) ([0b41df5](0b41df5ac8))
* merge conflicts ([8512eb4](8512eb4493))
* normalize date comparison to avoid datatype mismatch ([49b4830](49b4830785))
* **patch:** drop dead procedures first before other changes ([67d6761](67d67616ca))
* **payment_entry:** fix paid/received amount calculation for multi-currency accounts (backport [#54963](https://github.com/frappe/erpnext/issues/54963)) ([#54969](https://github.com/frappe/erpnext/issues/54969)) ([651af67](651af67b26))
* remove sql procedure method from AR report ([c705a93](c705a93776))
* stock balance showing incorrect value because of incorrect SLE ([dbacfd1](dbacfd13b8))
* **stock:** add whole number quantity validation in Stock Reconciliation (backport [#54922](https://github.com/frappe/erpnext/issues/54922)) ([#54924](https://github.com/frappe/erpnext/issues/54924)) ([48ed078](48ed07816d))
* **stock:** update buying amount calculation in gross profit report (backport [#55020](https://github.com/frappe/erpnext/issues/55020)) ([#55023](https://github.com/frappe/erpnext/issues/55023)) ([5e1880f](5e1880f09e))
* toast message for item price insert ([#55009](https://github.com/frappe/erpnext/issues/55009)) ([9309aec](9309aec209))
* validate company region in uae vat 201 (backport [#54899](https://github.com/frappe/erpnext/issues/54899)) ([#55054](https://github.com/frappe/erpnext/issues/55054)) ([5ad80b8](5ad80b8fb9))

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) ([#54907](https://github.com/frappe/erpnext/issues/54907)) ([6d3cd7d](6d3cd7d38a))
2026-05-20 04:10:36 +00:00
diptanilsaha
a4bdbec8f8 Merge pull request #55052 from frappe/version-15-hotfix
chore: release v15
2026-05-20 09:38:58 +05:30
Nabin Hait
0b41df5ac8 fix: item leaderboard uses Sales/Purchase Invoice instead of Orders (#55038)
Fixes https://github.com/frappe/erpnext/issues/46657
2026-05-20 00:49:39 +05:30
ruthra kumar
08f4437902 Merge pull request #55056 from frappe/mergify/bp/version-15-hotfix/pr-55053
fix(patch): drop dead procedures first before other changes (backport #55053)
2026-05-19 17:01:32 +05:30
mergify[bot]
5ad80b8fb9 fix: validate company region in uae vat 201 (backport #54899) (#55054)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix: validate company region in uae vat 201 (#54899)
2026-05-19 16:51:59 +05:30
ruthra kumar
67d67616ca fix(patch): drop dead procedures first before other changes
(cherry picked from commit 61d24ba55f)
2026-05-19 11:08:24 +00:00
rohitwaghchaure
1983204112 Merge pull request #55047 from frappe/mergify/bp/version-15-hotfix/pr-55046
fix: stock balance showing incorrect value because of incorrect SLE (backport #55046)
2026-05-19 14:14:47 +05:30
Rohit Waghchaure
dbacfd13b8 fix: stock balance showing incorrect value because of incorrect SLE
(cherry picked from commit 94b95d6c2f)
2026-05-19 08:22:09 +00:00
Ravibharathi
3b3e33d354 Merge pull request #55041 from frappe/mergify/bp/version-15-hotfix/pr-54761
fix: normalize date comparison to avoid datatype mismatch (backport #54761)
2026-05-19 11:53:32 +05:30
ervishnucs
49b4830785 fix: normalize date comparison to avoid datatype mismatch
(cherry picked from commit 01e382b106)

# Conflicts:
#	erpnext/accounts/party.py
2026-05-19 11:38:21 +05:30
mergify[bot]
5e1880f09e fix(stock): update buying amount calculation in gross profit report (backport #55020) (#55023)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): update buying amount calculation in gross profit report (#55020)
2026-05-19 09:44:37 +05:30
ruthra kumar
470bf628c7 Merge pull request #55014 from frappe/mergify/bp/version-15-hotfix/pr-55001
fix: remove sql procedure method from AR report (backport #55001)
2026-05-18 14:05:49 +05:30
ruthra kumar
c705a93776 fix: remove sql procedure method from AR report
(cherry picked from commit 63a7142b9b)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/accounts/report/accounts_receivable/accounts_receivable.py
#	erpnext/patches.txt
2026-05-18 13:49:25 +05:30
Nishka Gosalia
9ef783cd48 Merge pull request #55010 from frappe/mergify/bp/version-15-hotfix/pr-55009
fix: toast message for item price insert (backport #55009)
2026-05-18 12:04:48 +05:30
Nishka Gosalia
8512eb4493 fix: merge conflicts 2026-05-18 11:44:57 +05:30
Nishka Gosalia
9309aec209 fix: toast message for item price insert (#55009)
(cherry picked from commit ae9c632e39)

# Conflicts:
#	erpnext/stock/get_item_details.py
2026-05-18 06:11:11 +00:00
ruthra kumar
d02314935d Merge pull request #55003 from frappe/mergify/bp/version-15-hotfix/pr-51197
refactor: remove custom sql function in AR SQL procedure approach (backport #51197)
2026-05-18 11:11:54 +05:30
ruthra kumar
f5d83599cc refactor: remove custom sql function in AR SQL procedure approach
(cherry picked from commit d1e0992253)

# Conflicts:
#	erpnext/accounts/report/accounts_receivable/accounts_receivable.py
2026-05-18 10:55:04 +05:30
mergify[bot]
651af67b26 fix(payment_entry): fix paid/received amount calculation for multi-currency accounts (backport #54963) (#54969)
* fix(payment_entry): `paid_amount` and `received_amount` calculation depending upon `account_currency`

(cherry picked from commit 69642860ee)

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

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-15 10:33:07 +00:00
ruthra kumar
47c6bc4b91 Merge pull request #54959 from frappe/mergify/bp/version-15/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 13:43:24 +05:30
ruthra kumar
f037ee6501 refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 07:32:24 +00:00
ruthra kumar
2b2eb2fa27 Merge pull request #54956 from frappe/mergify/bp/version-15-hotfix/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 13:01:01 +05:30
ruthra kumar
d1d4480187 refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 06:50:58 +00:00
mergify[bot]
48ed07816d fix(stock): add whole number quantity validation in Stock Reconciliation (backport #54922) (#54924)
fix(stock): add whole number quantity validation in Stock Reconciliation (#54922)

(cherry picked from commit f9dec73042)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-13 15:20:13 +00:00
Frappe PR Bot
d43862624a chore(release): Bumped to Version 15.108.1
## [15.108.1](https://github.com/frappe/erpnext/compare/v15.108.0...v15.108.1) (2026-05-13)

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) (backport [#54907](https://github.com/frappe/erpnext/issues/54907)) ([#54917](https://github.com/frappe/erpnext/issues/54917)) ([dc4b9cc](dc4b9cc4bc))
2026-05-13 11:16:09 +00:00
mergify[bot]
dc4b9cc4bc Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (backport #54907) (#54917)
Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54907)

* Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: resolve conflicts

---------


(cherry picked from commit 6d3cd7d38a)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 16:44:53 +05:30
mergify[bot]
6d3cd7d38a Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54907)
* Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 09:58:45 +00:00
mergify[bot]
b969662b6c fix: add warehouse vaildation for repack entry (backport #54866) (#54900)
* fix: add warehouse vaildation for repack entry (#54866)

(cherry picked from commit bc07b2d3e5)

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

* chore: resolve conflicts

---------

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 06:42:18 +00:00
Frappe PR Bot
52d6b72a6b chore(release): Bumped to Version 15.108.0
# [15.108.0](https://github.com/frappe/erpnext/compare/v15.107.0...v15.108.0) (2026-05-12)

### Bug Fixes

* added permission validation for `deactivate_sales_person` (backport [#54884](https://github.com/frappe/erpnext/issues/54884)) ([#54885](https://github.com/frappe/erpnext/issues/54885)) ([9586bc7](9586bc7635))
* correct payment request function call in si and so ([603700a](603700aa0e))
* **crm:** handle empty _assign in appointment auto assignment (backport [#54782](https://github.com/frappe/erpnext/issues/54782)) ([#54794](https://github.com/frappe/erpnext/issues/54794)) ([6eaf92a](6eaf92aae6))
* decimal issue ([a5ff2ba](a5ff2bafe0))
* fetch get_item_tax_template while update items ([#54784](https://github.com/frappe/erpnext/issues/54784)) ([455bfcd](455bfcd750))
* fetch hour rate from workstation when operation hour_rate is mis… ([#54820](https://github.com/frappe/erpnext/issues/54820)) ([d57ec6c](d57ec6c094))
* incorrect serial nos picked during disassemble (backport [#54757](https://github.com/frappe/erpnext/issues/54757)) ([#54759](https://github.com/frappe/erpnext/issues/54759)) ([1e2a719](1e2a7196e5))
* incorrect validation thrown for drop shipped PI (backport [#54751](https://github.com/frappe/erpnext/issues/54751)) ([#54752](https://github.com/frappe/erpnext/issues/54752)) ([da95f83](da95f83686))
* raw material should not have target warehouse in manufacture entry (backport [#54849](https://github.com/frappe/erpnext/issues/54849)) ([#54860](https://github.com/frappe/erpnext/issues/54860)) ([bad85ad](bad85ad01b))
* **stock:** apply filters for rejected warehouse in pick list (backport [#54733](https://github.com/frappe/erpnext/issues/54733)) ([#54775](https://github.com/frappe/erpnext/issues/54775)) ([e5a6b5b](e5a6b5b3a0))
* **stock:** ignore reserved qty for stock levels in batch (backport [#54790](https://github.com/frappe/erpnext/issues/54790)) ([#54796](https://github.com/frappe/erpnext/issues/54796)) ([c3ac7aa](c3ac7aac66))
* **stock:** priorities pick list parent warehouse (backport [#54788](https://github.com/frappe/erpnext/issues/54788)) ([#54792](https://github.com/frappe/erpnext/issues/54792)) ([c3467cc](c3467cc169))
* **task:** update depends_on for closing date and review date [#54850](https://github.com/frappe/erpnext/issues/54850) (backport [#54852](https://github.com/frappe/erpnext/issues/54852)) ([#54862](https://github.com/frappe/erpnext/issues/54862)) ([213342a](213342a37c))
* validate variant values (backport [#54831](https://github.com/frappe/erpnext/issues/54831)) ([#54838](https://github.com/frappe/erpnext/issues/54838)) ([910fe9e](910fe9ef55))

### Features

* Philippines chart of account (backport [#53918](https://github.com/frappe/erpnext/issues/53918)) ([#54887](https://github.com/frappe/erpnext/issues/54887)) ([e9cfb04](e9cfb046a1))
2026-05-12 18:49:28 +00:00
diptanilsaha
594b5a2729 Merge pull request #54864 from frappe/version-15-hotfix
chore: release v15
2026-05-13 00:18:10 +05:30
mergify[bot]
e9cfb046a1 feat: Philippines chart of account (backport #53918) (#54887)
feat: Added Philippines chart of account json file (#53918)

* feat: Added philipinnes chart of account json file



* feat: made changes as per review comments and corrected indentation

* feat: made changes as per review comments

* feat: made changes as per review comments to resolve the issues

* fix: fixed changes as per review comments



* fix: fixed changes as per review comments on bank group account



---------




(cherry picked from commit 5560f6c270)

Signed-off-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Signed-off-by: soham7117 <sohampawar626@gmail.com>
Co-authored-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Co-authored-by: soham7117 <sohampawar626@gmail.com>
2026-05-12 16:40:56 +00:00
mergify[bot]
9586bc7635 fix: added permission validation for deactivate_sales_person (backport #54884) (#54885)
* fix: added permission validation for `deactivate_sales_person` (#54884)

(cherry picked from commit 9134db9cd3)

# Conflicts:
#	erpnext/setup/doctype/employee/employee.py

* chore: resolved conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-12 16:30:56 +00:00
mergify[bot]
213342a37c fix(task): update depends_on for closing date and review date #54850 (backport #54852) (#54862)
fix(task): update depends_on for closing date and review date #54850 (#54852)

(cherry picked from commit 3532c1cc69)

Co-authored-by: Jaypal Lakum <96212547+jp-the-dev@users.noreply.github.com>
2026-05-12 10:13:10 +00:00
mergify[bot]
bad85ad01b fix: raw material should not have target warehouse in manufacture entry (backport #54849) (#54860)
* fix: raw material should not have target warehouse in manufacture entry (#54849)

(cherry picked from commit b5527cf328)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-12 09:46:54 +00:00
Ravibharathi
684f072bca Merge pull request #54857 from aerele/v15-rename-payment-request-call
fix: correct payment request function call in si and so
2026-05-12 13:58:34 +05:30
sudarsan2001
603700aa0e fix: correct payment request function call in si and so 2026-05-12 13:47:32 +05:30
mergify[bot]
910fe9ef55 fix: validate variant values (backport #54831) (#54838)
fix: validate variant values (#54831)

(cherry picked from commit 95705f18aa)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 21:23:24 +05:30
Pandiyan P
d57ec6c094 fix: fetch hour rate from workstation when operation hour_rate is mis… (#54820)
fix: fetch hour rate from workstation when operation hour_rate is missing
2026-05-11 13:18:51 +05:30
mergify[bot]
6eaf92aae6 fix(crm): handle empty _assign in appointment auto assignment (backport #54782) (#54794)
fix(crm): handle empty _assign in appointment auto assignment (#54782)

(cherry picked from commit a4a389bd41)

Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
2026-05-08 12:48:15 +00:00
Ravibharathi
455bfcd750 fix: fetch get_item_tax_template while update items (#54784) 2026-05-08 12:47:33 +00:00
mergify[bot]
c3ac7aac66 fix(stock): ignore reserved qty for stock levels in batch (backport #54790) (#54796)
fix(stock): ignore reserved qty for stock levels in batch (#54790)

(cherry picked from commit 0b6a372a52)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-08 12:39:14 +00:00
mergify[bot]
c3467cc169 fix(stock): priorities pick list parent warehouse (backport #54788) (#54792)
fix(stock): priorities pick list parent warehouse (#54788)

(cherry picked from commit 4e850f31d5)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-05-08 12:34:59 +00:00
mergify[bot]
e5a6b5b3a0 fix(stock): apply filters for rejected warehouse in pick list (backport #54733) (#54775)
fix(stock): apply filters for rejected warehouse in pick list (#54733)

(cherry picked from commit 0fc96e8f7d)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-07 16:00:22 +05:30
mergify[bot]
1e2a7196e5 fix: incorrect serial nos picked during disassemble (backport #54757) (#54759)
fix: incorrect serial nos picked during disassemble

(cherry picked from commit 25f7fa548d)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-06 16:05:50 +05:30
mergify[bot]
da95f83686 fix: incorrect validation thrown for drop shipped PI (backport #54751) (#54752)
* fix: incorrect validation thrown for drop shipped PI (#54751)

(cherry picked from commit 907a809f3f)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-06 05:53:11 +00:00
rohitwaghchaure
84aa8e5c9f Merge pull request #54744 from frappe/mergify/bp/version-15-hotfix/pr-54723
fix: decimal issue in stock ageing report (backport #54723)
2026-05-05 22:02:43 +05:30
Frappe PR Bot
fc54fd09f1 chore(release): Bumped to Version 15.107.0
# [15.107.0](https://github.com/frappe/erpnext/compare/v15.106.0...v15.107.0) (2026-05-05)

### Bug Fixes

* accounts and account types in German CoA "SKR 03" ([#54711](https://github.com/frappe/erpnext/issues/54711)) ([581529f](581529fd00))
* copy project from first row to new rows (backport [#53295](https://github.com/frappe/erpnext/issues/53295)) ([#54619](https://github.com/frappe/erpnext/issues/54619)) ([698b087](698b087997))
* correct project filter in buying doctypes (backport [#54644](https://github.com/frappe/erpnext/issues/54644)) ([#54651](https://github.com/frappe/erpnext/issues/54651)) ([329f4e0](329f4e01a3))
* dont show serial/batch button when PR is submitted (backport [#54642](https://github.com/frappe/erpnext/issues/54642)) ([#54645](https://github.com/frappe/erpnext/issues/54645)) ([1b1bc3d](1b1bc3d81c))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54724](https://github.com/frappe/erpnext/issues/54724)) ([1a406e9](1a406e90c1))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54724](https://github.com/frappe/erpnext/issues/54724)) ([809feb9](809feb9c04))
* hide payment and payment request buttons based on permissions in invoices and orders (backport [#53920](https://github.com/frappe/erpnext/issues/53920)) ([#54735](https://github.com/frappe/erpnext/issues/54735)) ([9c9ecc7](9c9ecc77f8))
* incorrect expense account book in purchase return (backport [#54681](https://github.com/frappe/erpnext/issues/54681)) ([#54692](https://github.com/frappe/erpnext/issues/54692)) ([a3bb409](a3bb40904c))
* item query in quality inspection ([#54721](https://github.com/frappe/erpnext/issues/54721)) ([0b0f9d0](0b0f9d046d))
* **payment_entry:** convert the date args to string type before escaping in `get_outstanding_reference_documents` (backport [#54639](https://github.com/frappe/erpnext/issues/54639)) ([#54647](https://github.com/frappe/erpnext/issues/54647)) ([4bab1e4](4bab1e4142))
* **project:** use user.email for invitations and skip disabled users. (backport [#54561](https://github.com/frappe/erpnext/issues/54561)) ([#54666](https://github.com/frappe/erpnext/issues/54666)) ([58d95a3](58d95a35ff))
* **selling:** blanket order ordered qty recalculation on sales order status change (backport [#54593](https://github.com/frappe/erpnext/issues/54593)) ([#54622](https://github.com/frappe/erpnext/issues/54622)) ([d64b194](d64b19416e))
* set valid_from in created Item Price ([#54696](https://github.com/frappe/erpnext/issues/54696)) ([6246a9a](6246a9aa6e))
* show correct status in Serial No Ledger (backport [#54567](https://github.com/frappe/erpnext/issues/54567)) ([#54625](https://github.com/frappe/erpnext/issues/54625)) ([559b31b](559b31baae))
* show in and out qty in the stock ledger report for stock recos ([393fe75](393fe75363))
* use RecoverableErrors isinstance check for repost timeout status ([a49e2de](a49e2de866))

### Features

* copy terms attachments to transactions (backport [#53403](https://github.com/frappe/erpnext/issues/53403)) ([#54660](https://github.com/frappe/erpnext/issues/54660)) ([29282a8](29282a80cf))
2026-05-05 16:32:38 +00:00
diptanilsaha
31bf9bd1fd Merge pull request #54741 from frappe/version-15-hotfix 2026-05-05 22:00:21 +05:30
Rohit Waghchaure
a5ff2bafe0 fix: decimal issue
(cherry picked from commit 542eb6aca4)
2026-05-05 11:12:50 +00:00
mergify[bot]
9c9ecc77f8 fix: hide payment and payment request buttons based on permissions in invoices and orders (backport #53920) (#54735)
Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix: hide payment and payment request buttons based on permissions in invoices and orders (#53920)
2026-05-05 12:25:18 +05:30
mergify[bot]
1a406e90c1 fix: error when creating quotation from CRM (backport #54722) (#54724)
fix: error when creating quotation from CRM (#54722)

(cherry picked from commit 2d3190effb)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-04 21:30:11 +05:30
mergify[bot]
809feb9c04 fix: error when creating quotation from CRM (backport #54722) (#54724)
fix: error when creating quotation from CRM (#54722)

(cherry picked from commit 2d3190effb)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-04 21:30:02 +05:30
Mihir Kandoi
0b0f9d046d fix: item query in quality inspection (#54721) 2026-05-04 15:31:10 +00:00
mergify[bot]
d07d7feb3f refactor: Sales Partner Commission Summary and Sales Partner Transaction Summary report (backport #54268) (#54430)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-04 11:22:32 +05:30
Raffael Meyer
581529fd00 fix: accounts and account types in German CoA "SKR 03" (#54711) 2026-05-03 17:25:59 +00:00
Kaajalchhattani
6246a9aa6e fix: set valid_from in created Item Price (#54696)
Co-authored-by: Kaajal-Chhattani <kaajal.chhattani@aurigait.com>
2026-05-02 21:23:03 +05:30
mergify[bot]
29282a80cf feat: copy terms attachments to transactions (backport #53403) (#54660)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-05-01 13:04:07 +00:00
mergify[bot]
a3bb40904c fix: incorrect expense account book in purchase return (backport #54681) (#54692)
fix: incorrect expense account book in purchase return

(cherry picked from commit 2a720e7008)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-01 12:46:56 +05:30
Raffael Meyer
3919c3d385 refactor: re-save Item Tax Template (#54688) 2026-04-30 22:07:13 +00:00
mergify[bot]
58d95a35ff fix(project): use user.email for invitations and skip disabled users. (backport #54561) (#54666)
fix(project): use user.email for invitations and skip disabled users. (#54561)

* fix(project): use user.email for invitations and skip disabled users.

* Update erpnext/projects/doctype/project/project.py



* fix(project): remove duplicate loop causing indentation error

* fix(project): resolve pre-commit hook failure

---------


(cherry picked from commit 231dd1856f)

Co-authored-by: Hemil-Sangani <hemil@sanskartechnolab.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 14:35:29 +05:30
rohitwaghchaure
4bb450c294 Merge pull request #54670 from frappe/mergify/bp/version-15-hotfix/pr-54664
fix: show in and out qty in the stock ledger report for stock recos (backport #54664)
2026-04-30 14:33:42 +05:30
Rohit Waghchaure
393fe75363 fix: show in and out qty in the stock ledger report for stock recos
(cherry picked from commit da081254a6)
2026-04-30 08:44:10 +00:00
mergify[bot]
329f4e01a3 fix: correct project filter in buying doctypes (backport #54644) (#54651)
fix: correct project filter in buying doctypes (#54644)

(cherry picked from commit a04c028522)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 17:28:02 +05:30
mergify[bot]
4bab1e4142 fix(payment_entry): convert the date args to string type before escaping in get_outstanding_reference_documents (backport #54639) (#54647)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): convert the date args to string type before escaping in `get_outstanding_reference_documents` (#54639)
2026-04-29 11:35:49 +00:00
mergify[bot]
1b1bc3d81c fix: dont show serial/batch button when PR is submitted (backport #54642) (#54645)
* fix: dont show serial/batch button when PR is submitted (#54642)

(cherry picked from commit 060defcc2b)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:25:06 +00:00
rohitwaghchaure
88588769f1 Merge pull request #54543 from AssemBahnasy/fix/repost-recoverable-errors-status
fix: use RecoverableErrors isinstance check for repost timeout status
2026-04-29 16:50:55 +05:30
mergify[bot]
559b31baae fix: show correct status in Serial No Ledger (backport #54567) (#54625)
* refactor: extract SN status logic

(cherry picked from commit cb2e6e1e2e)

* fix: show correct status in Serial No Ledger

(cherry picked from commit 2b3e047143)

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2026-04-29 13:55:06 +05:30
mergify[bot]
d64b19416e fix(selling): blanket order ordered qty recalculation on sales order status change (backport #54593) (#54622)
fix(selling): blanket order ordered qty recalculation on sales order status change (#54593)

(cherry picked from commit d68801e73a)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-29 06:45:18 +00:00
mergify[bot]
698b087997 fix: copy project from first row to new rows (backport #53295) (#54619)
fix: copy project to new item row from parent

(cherry picked from commit 68cc518497)

Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
2026-04-29 11:55:36 +05:30
Frappe PR Bot
4dd9f0b255 chore(release): Bumped to Version 15.106.0
# [15.106.0](https://github.com/frappe/erpnext/compare/v15.105.0...v15.106.0) (2026-04-28)

### Bug Fixes

* **`get_stock_balance`:** validate inventory dimension fieldnames (backport [#54587](https://github.com/frappe/erpnext/issues/54587)) ([#54588](https://github.com/frappe/erpnext/issues/54588)) ([03f3a28](03f3a28f54))
* **accounts:** fetch project name from payment entry to journal entry ([55cce2a](55cce2a11c))
* add party_type for dynamic link and add it to grouping key ([a3ad1fb](a3ad1fb163))
* add project filter to accounts payable and receivable reports (backport [#54344](https://github.com/frappe/erpnext/issues/54344)) ([#54441](https://github.com/frappe/erpnext/issues/54441)) ([44f3f34](44f3f34c9e))
* avoid double reduction of pe reference outstanding (backport [#54193](https://github.com/frappe/erpnext/issues/54193)) ([#54612](https://github.com/frappe/erpnext/issues/54612)) ([51e7c66](51e7c66043))
* debit credit not equal in purchase transactions for multi currency (backport [#54456](https://github.com/frappe/erpnext/issues/54456)) ([#54563](https://github.com/frappe/erpnext/issues/54563)) ([78b2e45](78b2e45cb9))
* duplicate entries being shown in batch exists in future transact… (backport [#54604](https://github.com/frappe/erpnext/issues/54604)) ([#54605](https://github.com/frappe/erpnext/issues/54605)) ([176d980](176d980764))
* **edi:** restrict Code List imports to files and trusted backend URLs (backport [#54137](https://github.com/frappe/erpnext/issues/54137)) ([#54265](https://github.com/frappe/erpnext/issues/54265)) ([e0013f7](e0013f7618)), closes [#54488](https://github.com/frappe/erpnext/issues/54488)
* negative quantity check in validate_item_qty (backport [#54559](https://github.com/frappe/erpnext/issues/54559)) ([#54571](https://github.com/frappe/erpnext/issues/54571)) ([49ab25d](49ab25dda8))
* **payment_entry:** escape arguments on invoice and order fetching sql queries (backport [#54582](https://github.com/frappe/erpnext/issues/54582)) ([#54585](https://github.com/frappe/erpnext/issues/54585)) ([cceedd6](cceedd669f))
* **PCV:** set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (backport [#54522](https://github.com/frappe/erpnext/issues/54522)) ([#54523](https://github.com/frappe/erpnext/issues/54523)) ([6df39ae](6df39aec54))
* preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440)) ([#54492](https://github.com/frappe/erpnext/issues/54492)) ([722dc8c](722dc8c3f1))
* **purchase_register:** filter tax rows by parenttype in invoice tax map query (backport [#54272](https://github.com/frappe/erpnext/issues/54272)) ([#54443](https://github.com/frappe/erpnext/issues/54443)) ([4dff436](4dff436104))
* py error on stock ageing report (backport [#54467](https://github.com/frappe/erpnext/issues/54467)) ([#54468](https://github.com/frappe/erpnext/issues/54468)) ([6179449](6179449036))
* sales order is not valid when creating WO from MR from PP (backport [#54435](https://github.com/frappe/erpnext/issues/54435)) ([#54470](https://github.com/frappe/erpnext/issues/54470)) ([9a4c693](9a4c693f2d))
* **stock:** remove validation for transfer_qty field (backport [#54542](https://github.com/frappe/erpnext/issues/54542)) ([#54544](https://github.com/frappe/erpnext/issues/54544)) ([8569ff6](8569ff67ff))
* **stock:** set incoming rate as zero for outward sle (backport [#54514](https://github.com/frappe/erpnext/issues/54514)) ([#54532](https://github.com/frappe/erpnext/issues/54532)) ([68d213a](68d213a244))
* unknown column error on item code in quality inspection ([#54565](https://github.com/frappe/erpnext/issues/54565)) ([e7a29ab](e7a29abdb0))
* update status of quotation in patch (backport [#54577](https://github.com/frappe/erpnext/issues/54577)) ([#54579](https://github.com/frappe/erpnext/issues/54579)) ([1a8dc7e](1a8dc7e332))
* use key consistently ([8f9a5e6](8f9a5e6c0c))

### Features

* danish_bosnian_address_template (backport [#54093](https://github.com/frappe/erpnext/issues/54093)) ([#54515](https://github.com/frappe/erpnext/issues/54515)) ([973444e](973444e20e))

### Reverts

* Revert "fix: preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440))" ([#54507](https://github.com/frappe/erpnext/issues/54507)) ([1b08ac2](1b08ac248b))
* Revert "refactor: quality inspection item query (backport [#54511](https://github.com/frappe/erpnext/issues/54511))" ([#54557](https://github.com/frappe/erpnext/issues/54557)) ([f869e86](f869e86c9c)), closes [#54539](https://github.com/frappe/erpnext/issues/54539)
2026-04-28 21:00:34 +00:00
diptanilsaha
54b9392cc5 Merge pull request #54584 from frappe/version-15-hotfix 2026-04-29 02:28:40 +05:30
mergify[bot]
51e7c66043 fix: avoid double reduction of pe reference outstanding (backport #54193) (#54612)
* fix: avoid double reduction of pe reference outstanding (#54193)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
(cherry picked from commit d1a80d40c4)

# Conflicts:
#	erpnext/accounts/utils.py

* chore: resolved conflict

* chore: remove unused import of DateTimeLikeObject

---------

Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 20:35:46 +00:00
mergify[bot]
44f3f34c9e fix: add project filter to accounts payable and receivable reports (backport #54344) (#54441)
Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-28 22:39:42 +05:30
mergify[bot]
176d980764 fix: duplicate entries being shown in batch exists in future transact… (backport #54604) (#54605)
fix: duplicate entries being shown in batch exists in future transact… (#54604)

fix: duplicate entries being shown in batch exists in future transactions msg
(cherry picked from commit 54f20de7e3)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 22:23:03 +05:30
mergify[bot]
44af175556 refactor(sms_center): replaced raw SQL queries with Query Builder (backport #54600) (#54602)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 15:31:58 +00:00
mergify[bot]
03f3a28f54 fix(get_stock_balance): validate inventory dimension fieldnames (backport #54587) (#54588)
* fix(`get_stock_balance`): validate inventory dimension fieldnames (#54587)

(cherry picked from commit 084c7f72f0)

# Conflicts:
#	erpnext/stock/utils.py

* chore: resolved conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 18:14:07 +05:30
mergify[bot]
cceedd669f fix(payment_entry): escape arguments on invoice and order fetching sql queries (backport #54582) (#54585)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): escape arguments on invoice and order fetching sql queries (#54582)
2026-04-28 10:44:39 +00:00
mergify[bot]
1a8dc7e332 fix: update status of quotation in patch (backport #54577) (#54579)
fix: update status of quotation in patch (#54577)

(cherry picked from commit 2088a01c19)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 15:13:16 +05:30
mergify[bot]
49ab25dda8 fix: negative quantity check in validate_item_qty (backport #54559) (#54571)
fix: negative quantity check in validate_item_qty (#54559)

Fix negative quantity check in validate_item_qty

When saving a Blanket Order with a blank qty field in the items table, the following error is raised:

TypeError: '<' not supported between instances of 'NoneType' and 'int'

Root cause: The validate_item_qty method compares d.qty < 0 directly. When the qty field is left empty, its value is None, and Python cannot compare None with an integer.

Fix
Wrap d.qty with flt(), which safely converts None (and any non-numeric value) to 0.0 before the comparison.

# Before
if d.qty < 0:

# After
if flt(d.qty) < 0:

(cherry picked from commit 63edd5ddc6)

Co-authored-by: Vinay Mishra <39999379+vinaymishraofficial@users.noreply.github.com>
2026-04-28 05:30:26 +00:00
Mihir Kandoi
e7a29abdb0 fix: unknown column error on item code in quality inspection (#54565) 2026-04-28 10:19:43 +05:30
mergify[bot]
78b2e45cb9 fix: debit credit not equal in purchase transactions for multi currency (backport #54456) (#54563)
fix: debit credit not equal in purchase transactions for multi currency (#54456)

(cherry picked from commit 601581d6f8)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 15:14:58 +00:00
mergify[bot]
4dff436104 fix(purchase_register): filter tax rows by parenttype in invoice tax map query (backport #54272) (#54443)
Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-27 18:39:01 +05:30
Mihir Kandoi
f869e86c9c Revert "refactor: quality inspection item query (backport #54511)" (#54557)
Revert "refactor: quality inspection item query (backport #54511) (#54539)"

This reverts commit b01049814a.
2026-04-27 10:15:45 +00:00
mergify[bot]
8569ff67ff fix(stock): remove validation for transfer_qty field (backport #54542) (#54544)
fix(stock): remove validation for transfer_qty field (#54542)

(cherry picked from commit 60a6b38c31)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-27 07:12:24 +00:00
Assem Bahnasy
a49e2de866 fix: use RecoverableErrors isinstance check for repost timeout status
When a Repost Item Valuation job is killed by an RQ worker timeout
(JobTimeoutException raised via SIGALRM), the existing status detection
relied solely on traceback string matching for 'timeout' or 'Deadlock'.

This is unreliable because SIGALRM can interrupt a C-extension call
(e.g. inside pypika's copy.copy()) before Python records the exception
in the traceback. In that case the traceback shows only the interrupted
frame -- not JobTimeoutException -- so the job is permanently marked
'Failed' instead of 'In Progress', preventing the scheduler from
automatically retrying it.

RecoverableErrors = (JobTimeoutException, QueryDeadlockError,
QueryTimeoutError) is already defined at the top of this file and is
already used further down in the same except block to suppress email
notifications. Extend its use to also guard the status decision.

The traceback string fallback is kept as a secondary check for
forward compatibility with other timeout signals.

Fixes: jobs permanently stuck as 'Failed' after RQ worker timeout,
requiring manual re-queue to resume reposting.
2026-04-27 07:05:35 +00:00
mergify[bot]
b01049814a refactor: quality inspection item query (backport #54511) (#54539)
* refactor: quality inspection item query (#54511)

(cherry picked from commit be2a4b7b2a)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 05:51:18 +00:00
mergify[bot]
973444e20e feat: danish_bosnian_address_template (backport #54093) (#54515)
feat: danish_bosnian_address_template (#54093)

(cherry picked from commit e517eeaaa2)

Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com>
2026-04-26 21:06:37 +05:30
mergify[bot]
68d213a244 fix(stock): set incoming rate as zero for outward sle (backport #54514) (#54532)
fix(stock): set incoming rate as zero for outward sle

(cherry picked from commit ce37530e70)

Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-04-26 20:24:43 +05:30
mergify[bot]
6df39aec54 fix(PCV): set correct filters of from_date and to_date on General Ledger Report on clicking Ledger button (backport #54522) (#54523)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(PCV): set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (#54522)
2026-04-25 00:06:32 +05:30
mergify[bot]
071a28ff8c refactor: use consistent report column names (backport #54451) (#54518)
* refactor: use consistent report column names

(cherry picked from commit 7630c01e40)

* refactor: better label for entity type

(cherry picked from commit 8e12bda108)

* fix: add party_type for dynamic link and add it to grouping key

(cherry picked from commit a3ad1fb163)

* fix: use key consistently

(cherry picked from commit 8f9a5e6c0c)

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
2026-04-24 14:32:10 +00:00
Mihir Kandoi
1b08ac248b Revert "fix: preserve inventory dimensions when raw materials are reset (backport #54440)" (#54507)
Revert "fix: preserve inventory dimensions when raw materials are reset (back…"

This reverts commit 722dc8c3f1.
2026-04-24 08:43:59 +00:00
mergify[bot]
722dc8c3f1 fix: preserve inventory dimensions when raw materials are reset (backport #54440) (#54492)
* fix: preserve inventory dimensions when raw materials are reset (#54440)

* fix: preserve inventory dimensions when raw materials are reset

* test: add test case

(cherry picked from commit 0e20e35842)

# Conflicts:
#	erpnext/patches.txt
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-23 17:27:07 +00:00
mergify[bot]
e0013f7618 fix(edi): restrict Code List imports to files and trusted backend URLs (backport #54137) (#54265)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(edi): restrict Code List imports to files and trusted backend URLs (#54137)
fix(edi): hardcode "Code List" DocType in importer (#54488)
2026-04-23 15:37:39 +00:00
Smit Vora
017635ab04 Merge pull request #54451 from vorasmit/tds-reports-refactor-backport 2026-04-23 15:25:46 +05:30
Smit Vora
8f9a5e6c0c fix: use key consistently 2026-04-23 15:01:46 +05:30
mergify[bot]
9a4c693f2d fix: sales order is not valid when creating WO from MR from PP (backport #54435) (#54470)
fix: sales order is not valid when creating WO from MR from PP (#54435)

(cherry picked from commit e65b9fc2ae)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-22 15:05:29 +00:00
mergify[bot]
6179449036 fix: py error on stock ageing report (backport #54467) (#54468)
fix: py error on stock ageing report (#54467)

(cherry picked from commit f5357c233d)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-22 14:32:21 +00:00
Smit Vora
a3ad1fb163 fix: add party_type for dynamic link and add it to grouping key 2026-04-22 12:12:18 +05:30
Smit Vora
8e12bda108 refactor: better label for entity type 2026-04-22 12:11:04 +05:30
Ravibharathi
947b282e0c Merge pull request #54452 from frappe/mergify/bp/version-15-hotfix/pr-54307
fix(accounts): fetch project name from payment entry to journal entry (backport #54307)
2026-04-22 11:24:29 +05:30
sarathibalamurugan
f9ae22d85e test: add test for project name in exchange gain loss entry
(cherry picked from commit 9eeb819106)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/test_payment_entry.py
2026-04-22 11:05:18 +05:30
Frappe PR Bot
b6902ef960 chore(release): Bumped to Version 15.105.0
# [15.105.0](https://github.com/frappe/erpnext/compare/v15.104.3...v15.105.0) (2026-04-21)

### Bug Fixes

* add portal user ownership check to supplier quotation (backport [#54298](https://github.com/frappe/erpnext/issues/54298)) ([#54299](https://github.com/frappe/erpnext/issues/54299)) ([1e4cafa](1e4cafaa0e))
* changed qty validation from qty field to stock_qty (backport [#54352](https://github.com/frappe/erpnext/issues/54352)) ([#54356](https://github.com/frappe/erpnext/issues/54356)) ([1ccbc9f](1ccbc9f621))
* clear conditions table when calculate_based_on is set to Fixed ([35bd437](35bd43775c))
* clear shipping rule conditions for fixed shipping rule ([9e10ecc](9e10ecc4cb))
* **dashboard-trends:** set default fiscal year and company before val… (backport [#54339](https://github.com/frappe/erpnext/issues/54339)) ([#54399](https://github.com/frappe/erpnext/issues/54399)) ([799f897](799f897036))
* fetch item tax template from item group when creating item ([#54405](https://github.com/frappe/erpnext/issues/54405)) ([ffa0268](ffa0268a57))
* move make_dimension_in_accounting_doctypes from after_insert to on_update (backport [#54172](https://github.com/frappe/erpnext/issues/54172)) ([#54317](https://github.com/frappe/erpnext/issues/54317)) ([d9d8fc6](d9d8fc6912))
* negative batch report showing same batch-warehouse multiple times ([3229fce](3229fce9a5))
* non-collapsible in customer quick entry ([9ee0594](9ee059465a))
* **pos_invoice_item:** fetch `grant_commission` from `item_code` (backport [#54413](https://github.com/frappe/erpnext/issues/54413)) ([#54417](https://github.com/frappe/erpnext/issues/54417)) ([813f464](813f4644a0))
* reset base_rounded_total when rounded_total resets (backport [#54241](https://github.com/frappe/erpnext/issues/54241)) ([#54303](https://github.com/frappe/erpnext/issues/54303)) ([28367ac](28367ac966))
* **vat audit report:** fallback to item name when item code is missing ([#54049](https://github.com/frappe/erpnext/issues/54049)) ([2c1ea8d](2c1ea8d30c))

### Features

* enhance tax withholding details report with additional columns support (backport [#54409](https://github.com/frappe/erpnext/issues/54409)) ([#54432](https://github.com/frappe/erpnext/issues/54432)) ([e223260](e22326065d))
2026-04-21 19:53:50 +00:00
diptanilsaha
489ff20021 Merge pull request #54438 from frappe/version-15-hotfix 2026-04-22 01:22:15 +05:30
diptanilsaha
bd957a9bbc Revert "feat: enhance tax withholding details report with additional columns support (backport #54409)" (#54458) 2026-04-21 18:49:08 +00:00
Lakshit Jain
e22326065d feat: enhance tax withholding details report with additional columns support (backport #54409) (#54432) 2026-04-22 00:02:19 +05:30
sarathibalamurugan
55cce2a11c fix(accounts): fetch project name from payment entry to journal entry
(cherry picked from commit d9b255b952)
2026-04-21 13:29:16 +00:00
Smit Vora
7630c01e40 refactor: use consistent report column names 2026-04-21 18:53:25 +05:30
Ravibharathi
bd4eb71205 Merge pull request #54423 from frappe/mergify/bp/version-15-hotfix/pr-54415
fix: clear conditions table when calculate_based_on is set to Fixed (backport #54415)
2026-04-20 19:44:36 +05:30
ravibharathi656
9e10ecc4cb fix: clear shipping rule conditions for fixed shipping rule
(cherry picked from commit d6bb0ae093)
2026-04-20 13:53:03 +00:00
sarathibalamurugan
35bd43775c fix: clear conditions table when calculate_based_on is set to Fixed
(cherry picked from commit d73920be12)
2026-04-20 13:53:02 +00:00
mergify[bot]
813f4644a0 fix(pos_invoice_item): fetch grant_commission from item_code (backport #54413) (#54417)
* fix(pos_invoice_item): fetch `grant_commission` from `item_code` (#54413)

(cherry picked from commit 6c51e4cd1f)

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

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-20 11:48:06 +00:00
Ravibharathi
2c1ea8d30c fix(vat audit report): fallback to item name when item code is missing (#54049)
* fix(vat audit report): fallback to item name when item code is missing

* fix: validate south africa company selection

* fix: simplify parent item lookup

* fix: handle missing item mapping

* fix: use list instead of set
2026-04-20 15:56:37 +05:30
Pandiyan P
ffa0268a57 fix: fetch item tax template from item group when creating item (#54405) 2026-04-20 11:58:16 +05:30
mergify[bot]
1ccbc9f621 fix: changed qty validation from qty field to stock_qty (backport #54352) (#54356)
fix: changed qty validation from qty field to stock_qty (#54352)

(cherry picked from commit ba01d66c24)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-04-20 10:53:44 +05:30
mergify[bot]
799f897036 fix(dashboard-trends): set default fiscal year and company before val… (backport #54339) (#54399)
* fix(dashboard-trends): set default fiscal year and company before val… (#54339)

* fix(dashboard-trends): set default fiscal year and company before validating filters Ensure  and  are populated with default values

* fix(dashboard-trends): ensure fiscal_year and company are properly set before validation to avoid empty filter issues

* Update erpnext/controllers/trends.py

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit d61b5fd5f6)

# Conflicts:
#	erpnext/controllers/trends.py

* chore: resolve conflicts

---------

Co-authored-by: Ahmed AbuKhatwa <82771130+AhmedAbokhatwa@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 19:34:19 +05:30
mergify[bot]
6b7bdfdfd3 Fix : None handling in pricing rule free item quantity calculation (backport #54375) (#54395)
Fix : None handling in pricing rule free item quantity calculation (#54375)

* fix(pricing_rule): handle None qty in transaction_qty calculation

* Update erpnext/accounts/doctype/pricing_rule/utils.py

---------



(cherry picked from commit 82438d6c72)

Co-authored-by: Jaganath-Tridots <jaganath@tridotstech.com>
Co-authored-by: Jagan <jagan@DESKTOP-HPDMQ06.localdomain>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 07:57:37 +00:00
rohitwaghchaure
e3374933ed Merge pull request #54359 from frappe/mergify/bp/version-15-hotfix/pr-54354
fix: negative batch report showing same batch-warehouse multiple times (backport #54354)
2026-04-17 21:26:16 +05:30
Rohit Waghchaure
3229fce9a5 fix: negative batch report showing same batch-warehouse multiple times
(cherry picked from commit 700572980d)
2026-04-17 15:41:47 +00:00
mergify[bot]
d9d8fc6912 fix: move make_dimension_in_accounting_doctypes from after_insert to on_update (backport #54172) (#54317)
* fix: move make_dimension_in_accounting_doctypes from after_insert to on_update

(cherry picked from commit ee067e6015)

# Conflicts:
#	erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py

* chore: resolve conflicts in accounting_dimension.py

---------

Co-authored-by: Shllokkk <shllokosan23@gmail.com>
2026-04-17 15:14:17 +05:30
mergify[bot]
28367ac966 fix: reset base_rounded_total when rounded_total resets (backport #54241) (#54303)
* fix: reset base_rounded_total when rounded_total resets

(cherry picked from commit f8d278b733)

# Conflicts:
#	erpnext/controllers/tests/test_taxes_and_totals.py
#	erpnext/public/js/controllers/taxes_and_totals.js

* chore: spelling mistake

(cherry picked from commit e2ac476587)

* chore: resolve conflicts

---------

Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-16 10:39:57 +05:30
NaviN
67632e81d0 Merge pull request #54308 from frappe/mergify/bp/version-15-hotfix/pr-54306
fix: non-collapsible in customer quick entry (backport #54306)
2026-04-15 17:32:16 +05:30
PKSowmiya05
9ee059465a fix: non-collapsible in customer quick entry
(cherry picked from commit 53e120269d)
2026-04-15 11:56:39 +00:00
mergify[bot]
1e4cafaa0e fix: add portal user ownership check to supplier quotation (backport #54298) (#54299)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: add portal user ownership check to supplier quotation (#54298)
2026-04-15 06:07:23 +00:00
Frappe PR Bot
fc3ceff42f chore(release): Bumped to Version 15.104.3
## [15.104.3](https://github.com/frappe/erpnext/compare/v15.104.2...v15.104.3) (2026-04-14)

### Bug Fixes

* account change in warehouse (backport [#54182](https://github.com/frappe/erpnext/issues/54182)) ([#54204](https://github.com/frappe/erpnext/issues/54204)) ([430705f](430705f56c))
* hardcoded precision causing decimal issues ([7754504](77545042a5))
* inventory dimension patch (backport [#54141](https://github.com/frappe/erpnext/issues/54141)) ([#54145](https://github.com/frappe/erpnext/issues/54145)) ([deb67db](deb67db4a0))
* inventory dimension patch (backport [#54147](https://github.com/frappe/erpnext/issues/54147)) ([#54148](https://github.com/frappe/erpnext/issues/54148)) ([a56d698](a56d6984d1))
* inventory dimensions should not be mandatory unnecesarily (backport [#54064](https://github.com/frappe/erpnext/issues/54064)) ([#54133](https://github.com/frappe/erpnext/issues/54133)) ([a26c845](a26c845332))
* last SLE not updated in the file ([8408e81](8408e81335))
* **list_opportunity_report:** parameterized `lost_reason` ([#54160](https://github.com/frappe/erpnext/issues/54160)) ([1604c21](1604c21602))
* make operation mandatory when any sub operation row is added (backport [#54245](https://github.com/frappe/erpnext/issues/54245)) ([#54247](https://github.com/frappe/erpnext/issues/54247)) ([cbe5ad6](cbe5ad6337))
* preserve asset movement field properties after save ([a87015e](a87015e8e6))
* quality inspection item code fetch perm issue (backport [#54121](https://github.com/frappe/erpnext/issues/54121)) ([#54126](https://github.com/frappe/erpnext/issues/54126)) ([bcd6d99](bcd6d99549))
* remove unneccessary function for serial no status updation (backport [#54191](https://github.com/frappe/erpnext/issues/54191)) ([#54196](https://github.com/frappe/erpnext/issues/54196)) ([cb24d94](cb24d9404d))
* **sales invoice:** toggle Get Items From button based on is_return and POS view (backport [#52594](https://github.com/frappe/erpnext/issues/52594)) ([#54138](https://github.com/frappe/erpnext/issues/54138)) ([5de4102](5de4102dda))
* sanitize genericode import inputs and secure XML parser (backport [#53302](https://github.com/frappe/erpnext/issues/53302)) ([#54174](https://github.com/frappe/erpnext/issues/54174)) ([76e910e](76e910e8c0))
* set default posting time in RIV ([6e438e7](6e438e71eb))
* **stock:** remove float precision to fix precision issue (backport [#54284](https://github.com/frappe/erpnext/issues/54284)) ([#54288](https://github.com/frappe/erpnext/issues/54288)) ([0e9b3b4](0e9b3b459a))
* **stock:** update bin to zero when no previous sle exists (backport [#54236](https://github.com/frappe/erpnext/issues/54236)) ([#54263](https://github.com/frappe/erpnext/issues/54263)) ([46a1c6f](46a1c6fda0))
* update return value in workstation list view indicator (backport [#54198](https://github.com/frappe/erpnext/issues/54198)) ([#54200](https://github.com/frappe/erpnext/issues/54200)) ([0a3f9f0](0a3f9f0b9f))
* update_nsm only in warehouse creation ([#54165](https://github.com/frappe/erpnext/issues/54165)) ([e9c1a09](e9c1a09af3))
2026-04-14 18:20:17 +00:00
diptanilsaha
c74a44e526 Merge pull request #54282 from frappe/version-15-hotfix 2026-04-14 23:48:43 +05:30
mergify[bot]
8b3d65ae78 Revert "fix: sync paid and received amount" (backport #54238) (#54292)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix: sync paid and received amount" (#54238)
2026-04-14 22:27:04 +05:30
mergify[bot]
0e9b3b459a fix(stock): remove float precision to fix precision issue (backport #54284) (#54288)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): remove float precision to fix precision issue (#54284)
2026-04-14 11:33:23 +00:00
mergify[bot]
46a1c6fda0 fix(stock): update bin to zero when no previous sle exists (backport #54236) (#54263)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): update bin to zero when no previous sle exists (#54236)
2026-04-13 15:49:45 +00:00
mergify[bot]
cbe5ad6337 fix: make operation mandatory when any sub operation row is added (backport #54245) (#54247)
Co-authored-by: Sudarshan <73628063+sudarsan2001@users.noreply.github.com>
fix: make operation mandatory when any sub operation row is added (#54245)
2026-04-13 21:06:05 +05:30
mergify[bot]
17ce550417 Fix(bom): refetch the rate of item when 'source_from_supplier' is updated (backport #54187) (#54207)
Co-authored-by: Sambhav Saxena <76242518+sambhavsaxena@users.noreply.github.com>
Fix(bom): refetch the rate of item when 'source_from_supplier' is updated (#54187)
2026-04-10 23:44:55 +05:30
mergify[bot]
430705f56c fix: account change in warehouse (backport #54182) (#54204)
Co-authored-by: nishkagosalia <nishka.gosalia@gmail.com>
2026-04-10 20:32:33 +05:30
mergify[bot]
0a3f9f0b9f fix: update return value in workstation list view indicator (backport #54198) (#54200)
Co-authored-by: Praveenkumar Dhanasekar <164200710+Praveenku-mar@users.noreply.github.com>
fix: update return value in workstation list view indicator (#54198)
2026-04-10 16:50:32 +05:30
mergify[bot]
cb24d9404d fix: remove unneccessary function for serial no status updation (backport #54191) (#54196)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: remove unneccessary function for serial no status updation (#54191)
2026-04-10 10:53:04 +00:00
Nishka Gosalia
bc6780d4c7 Merge pull request #54179 from frappe/revert-54170-mergify/bp/version-15-hotfix/pr-54165
fix: update_nsm only in warehouse creation (backport #54165)"
2026-04-09 18:31:00 +05:30
Nishka Gosalia
8b16c310f4 Revert "fix: update_nsm only in warehouse creation (backport #54165)" 2026-04-09 18:12:46 +05:30
mergify[bot]
76e910e8c0 fix: sanitize genericode import inputs and secure XML parser (backport #53302) (#54174)
Co-authored-by: Shllokkk <shllokosan23@gmail.com>
2026-04-09 11:30:24 +00:00
Frappe PR Bot
8aede87290 chore(release): Bumped to Version 15.104.2
## [15.104.2](https://github.com/frappe/erpnext/compare/v15.104.1...v15.104.2) (2026-04-09)

### Bug Fixes

* set default posting time in RIV ([041f99c](041f99c926))
2026-04-09 11:15:07 +00:00
rohitwaghchaure
1b2c7ca21f Merge pull request #54169 from frappe/mergify/bp/version-15/pr-54162
fix: set default posting time in RIV (backport #54161) (backport #54162)
2026-04-09 16:43:37 +05:30
Nishka Gosalia
c44ec7eab4 Merge pull request #54170 from frappe/mergify/bp/version-15-hotfix/pr-54165
fix: update_nsm only in warehouse creation (backport #54165)
2026-04-09 16:29:32 +05:30
Nishka Gosalia
e9c1a09af3 fix: update_nsm only in warehouse creation (#54165)
(cherry picked from commit b0e3fa3979)
2026-04-09 10:28:14 +00:00
rohitwaghchaure
db3a40409f chore: fix conflicts
Removed unused method reset_repost_only_accounting_ledgers and fixed the validate method to set default posting time.

(cherry picked from commit 2df574baae)
2026-04-09 09:58:50 +00:00
Rohit Waghchaure
041f99c926 fix: set default posting time in RIV
(cherry picked from commit a7ece65536)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
(cherry picked from commit 6e438e71eb)
2026-04-09 09:58:50 +00:00
rohitwaghchaure
8843068da9 Merge pull request #54162 from frappe/mergify/bp/version-15-hotfix/pr-54161
fix: set default posting time in RIV (backport #54161)
2026-04-09 15:28:12 +05:30
rohitwaghchaure
2df574baae chore: fix conflicts
Removed unused method reset_repost_only_accounting_ledgers and fixed the validate method to set default posting time.
2026-04-09 14:24:07 +05:30
Rohit Waghchaure
6e438e71eb fix: set default posting time in RIV
(cherry picked from commit a7ece65536)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
2026-04-09 08:26:57 +00:00
diptanilsaha
1604c21602 fix(list_opportunity_report): parameterized lost_reason (#54160) 2026-04-09 07:23:12 +00:00
Aarol D'Souza
97c4cd140b Merge pull request #54157 from frappe/mergify/bp/version-15-hotfix/pr-54129
refactor: update reset password method name (backport #54129)
2026-04-09 12:30:37 +05:30
mergify[bot]
9d64d4ac05 Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-54129 2026-04-09 06:42:06 +00:00
mergify[bot]
5de4102dda fix(sales invoice): toggle Get Items From button based on is_return and POS view (backport #52594) (#54138)
Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com>
Co-authored-by: Navin-S-R <navin@aerele.in>
fix(sales invoice): toggle Get Items From button based on is_return and POS view (#52594)
2026-04-09 11:58:07 +05:30
AarDG10
39a473455d refactor: update reset password method name
(cherry picked from commit c4d74483e1)
2026-04-09 06:23:37 +00:00
Frappe PR Bot
b88f3f69b0 chore(release): Bumped to Version 15.104.1
## [15.104.1](https://github.com/frappe/erpnext/compare/v15.104.0...v15.104.1) (2026-04-09)

### Bug Fixes

* last SLE not updated in the file ([3a2dc6f](3a2dc6f9ee))
2026-04-09 05:20:24 +00:00
rohitwaghchaure
dba8abbabf Merge pull request #54154 from frappe/mergify/bp/version-15/pr-54150
fix: last SLE not updated in the file (backport #54132) (backport #54150)
2026-04-09 10:48:53 +05:30
rohitwaghchaure
c1591c37db chore: fix conflicts
(cherry picked from commit c70259687a)
2026-04-09 04:48:38 +00:00
Rohit Waghchaure
3a2dc6f9ee fix: last SLE not updated in the file
(cherry picked from commit 38ed425ee2)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
(cherry picked from commit 8408e81335)
2026-04-09 04:48:38 +00:00
rohitwaghchaure
4f1203dbd0 Merge pull request #54150 from frappe/mergify/bp/version-15-hotfix/pr-54132
fix: last SLE not updated in the file (backport #54132)
2026-04-09 10:17:56 +05:30
rohitwaghchaure
c70259687a chore: fix conflicts 2026-04-09 09:06:31 +05:30
Rohit Waghchaure
8408e81335 fix: last SLE not updated in the file
(cherry picked from commit 38ed425ee2)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2026-04-09 02:53:01 +00:00
mergify[bot]
a56d6984d1 fix: inventory dimension patch (backport #54147) (#54148) 2026-04-09 02:40:40 +00:00
mergify[bot]
deb67db4a0 fix: inventory dimension patch (backport #54141) (#54145)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: inventory dimension patch (#54141)
2026-04-09 02:01:45 +00:00
mergify[bot]
a26c845332 fix: inventory dimensions should not be mandatory unnecesarily (backport #54064) (#54133)
* fix: inventory dimensions should not be mandatory unnecesarily (#54064)

(cherry picked from commit 6e44b8913e)

# Conflicts:
#	erpnext/patches.txt
#	erpnext/stock/doctype/inventory_dimension/inventory_dimension.py

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-08 14:23:20 +00:00
mergify[bot]
bcd6d99549 fix: quality inspection item code fetch perm issue (backport #54121) (#54126)
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: quality inspection item code fetch perm issue (#54121)
2026-04-08 12:05:39 +00:00
rohitwaghchaure
d39d076fba Merge pull request #54118 from frappe/mergify/bp/version-15-hotfix/pr-54102
fix: hardcoded precision causing decimal issues (backport #54102)
2026-04-08 14:25:07 +05:30
rohitwaghchaure
21607f39c5 chore: fix conflicts 2026-04-08 14:05:04 +05:30
rohitwaghchaure
39a4760e07 chore: fix conflicts 2026-04-08 12:29:00 +05:30
rohitwaghchaure
d2c6a8958d chore: fix conflicts
Updated the modified date for the delivery note item.
2026-04-08 12:28:29 +05:30
Rohit Waghchaure
77545042a5 fix: hardcoded precision causing decimal issues
(cherry picked from commit 90fd6f2e40)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#	erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
2026-04-08 06:49:57 +00:00
Khushi Rawat
13f4ba1857 Merge pull request #54116 from frappe/mergify/bp/version-15-hotfix/pr-54103
fix: preserve asset movement field properties after save (backport #54103)
2026-04-08 12:15:45 +05:30
ravibharathi656
a87015e8e6 fix: preserve asset movement field properties after save
(cherry picked from commit 4a004a2a82)
2026-04-08 06:28:09 +00:00
Frappe PR Bot
a2626ed55f chore(release): Bumped to Version 15.104.0
# [15.104.0](https://github.com/frappe/erpnext/compare/v15.103.1...v15.104.0) (2026-04-07)

### Bug Fixes

* add support to fetch items based on manufacture stock entry; fix how it's done from work order ([e9ce0a4](e9ce0a41e6))
* add v15 compatibility for scrap item ([652bd39](652bd396d4))
* auto-set source_stock_entry ([b87b445](b87b445802))
* avg stock entries for disassembly from WO ([44d4079](44d40795df))
* correct warehouse preference for disassemble ([b8ddc2f](b8ddc2f2b9))
* create source_stock_entry to refer to original manufacturing entry ([55ee1dc](55ee1dcd04))
* custom button to disassemble manufactured stock entry with work order ([835ae27](835ae27b38))
* disassembly prompt with source stock entry field ([44f2e94](44f2e9480d))
* do not repost GL if no change in valuation ([0063201](0063201818))
* do not show inv dimension unnecessarily in stock entry (backport [#53946](https://github.com/frappe/erpnext/issues/53946)) ([#53950](https://github.com/frappe/erpnext/issues/53950)) ([e159c79](e159c79766))
* ensure compatibility with v15 ([8b42fcf](8b42fcf274))
* GL entries for different exchange rate in the purchase invoice ([def62cf](def62cf3fe))
* handle disassembly for secondary / scrap items ([229dc23](229dc23f97))
* include rejected qty in tax (purchase receipt) (backport [#53624](https://github.com/frappe/erpnext/issues/53624)) ([#53971](https://github.com/frappe/erpnext/issues/53971)) ([3fbfad1](3fbfad1b9b))
* manufacture entry with group_by support ([841b507](841b507502))
* **manufacturing:** handle null cur_dialog in BOM work order dialog (backport [#54011](https://github.com/frappe/erpnext/issues/54011)) ([#54014](https://github.com/frappe/erpnext/issues/54014)) ([cb0a548](cb0a548a95))
* not able to set operation in work order ([62d5870](62d58702a0))
* prevent selection of group type customer group in customer master ([7a227e0](7a227e048e))
* process loss with bom path disassembly ([eee6d7e](eee6d7e566))
* **promotional_scheme:** toggle enable state between Buying and Selli… (backport [#54110](https://github.com/frappe/erpnext/issues/54110)) ([#54111](https://github.com/frappe/erpnext/issues/54111)) ([5b7e6eb](5b7e6eb831))
* remove reference in serial/batch when document is cancelled (backport [#53979](https://github.com/frappe/erpnext/issues/53979)) ([#53988](https://github.com/frappe/erpnext/issues/53988)) ([e33abee](e33abeef7f))
* remove unnecessary param, and use value from self ([0b0dccd](0b0dccd294))
* resolve user permission error on status change by updating user … (backport [#54033](https://github.com/frappe/erpnext/issues/54033)) ([#54059](https://github.com/frappe/erpnext/issues/54059)) ([14085de](14085de332))
* set bom details on disassembly; abs batch qty ([84d5b52](84d5b52483))
* set serial and batch from source stock entry - on disassemble ([df049cd](df049cd277))
* set_query for source stock entry ([849b2e6](849b2e6ebf))
* show current stock qty in Stock Entry PDF (backport [#53761](https://github.com/frappe/erpnext/issues/53761)) ([#54031](https://github.com/frappe/erpnext/issues/54031)) ([af0116c](af0116cdc5))
* skip discount amount validation when not saving ([13eab9f](13eab9f993))
* **stock:** update stock queue in SABE for return entries ([05d6cf5](05d6cf5c9a))
* support creating disassembly (without link of WO) ([ef15c05](ef15c0581d))
* sync paid and received amount (backport [#53039](https://github.com/frappe/erpnext/issues/53039)) ([#54107](https://github.com/frappe/erpnext/issues/54107)) ([0505684](0505684d22))
* **test:** do not use is_group enabled customer group in test ([97684d3](97684d3dae))
* **test:** pin posting date in test_depreciation_on_cancel_invoice ([7f72189](7f72189665))
* **test:** use non-group customer group in test setup ([ea3fcc2](ea3fcc214b))
* transactions where update stock is 0 should not create SLEs (backport [#54035](https://github.com/frappe/erpnext/issues/54035)) ([#54076](https://github.com/frappe/erpnext/issues/54076)) ([bcf59e7](bcf59e7171))
* update min date based on transaction_date (backport [#53803](https://github.com/frappe/erpnext/issues/53803)) ([#54024](https://github.com/frappe/erpnext/issues/54024)) ([a71d32e](a71d32e668))
* use get_value ([8f01d12](8f01d12b5e))
* **ux:** refresh grid to correctly persist the state of fields ([3c327d5](3c327d5225))
* validate qty that can be disassembled from source stock entry. ([583c7b9](583c7b9819))
* validate work order consistency in stock entry ([d690a0c](d690a0c6bd))
* validation test for customer group ([7794f30](7794f3033e))
* **warehouse_capacity_dashboard:** removed `escape` from template (backport [#53907](https://github.com/frappe/erpnext/issues/53907)) ([#53908](https://github.com/frappe/erpnext/issues/53908)) ([efdb004](efdb004f0b))

### Features

* Allow Editing of Items and Quantities in Work Order ([1d36cb5](1d36cb55cd))
* croatian_address_template (backport [#53888](https://github.com/frappe/erpnext/issues/53888)) ([#54057](https://github.com/frappe/erpnext/issues/54057)) ([ee81268](ee812687e6))
* **timesheet:** allow partial billing and handled return ([21805bd](21805bde1f))

### Reverts

* botched backport ([#53967](https://github.com/frappe/erpnext/issues/53967)) ([22774fd](22774fdf87)), closes [#53776](https://github.com/frappe/erpnext/issues/53776) [#53766](https://github.com/frappe/erpnext/issues/53766) [#53767](https://github.com/frappe/erpnext/issues/53767)
2026-04-07 18:01:31 +00:00
diptanilsaha
0cc77274cb Merge pull request #54101 from frappe/version-15-hotfix 2026-04-07 22:19:03 +05:30
mergify[bot]
5b7e6eb831 fix(promotional_scheme): toggle enable state between Buying and Selli… (backport #54110) (#54111)
Co-authored-by: Ahmed AbuKhatwa <82771130+AhmedAbokhatwa@users.noreply.github.com>
Co-authored-by: AhmedAbukhatwa <Ahmedabukhatwa1@gmail.com>
fix(promotional_scheme): toggle enable state between Buying and Selli… (#54110)
2026-04-07 21:55:06 +05:30
rohitwaghchaure
1fb9c5244c Merge pull request #54009 from frappe/mergify/bp/version-15-hotfix/pr-53994
fix(stock): update stock queue in SABE for return entries (backport #53994)
2026-04-07 19:29:19 +05:30
rohitwaghchaure
e68eece3da Merge pull request #53804 from frappe/mergify/bp/version-15-hotfix/pr-52152
Refactor reposting feature (backport #52152)
2026-04-07 19:28:35 +05:30
Smit Vora
b8063a07fc Merge pull request #54097 from frappe/mergify/bp/version-15-hotfix/pr-53964
fix: consistently disassemble based on source  > SE / WO / BOM (backport #53964)
2026-04-07 19:27:23 +05:30
Smit Vora
8b42fcf274 fix: ensure compatibility with v15 2026-04-07 19:09:51 +05:30
Rohit Waghchaure
0063201818 fix: do not repost GL if no change in valuation 2026-04-07 18:52:10 +05:30
Rohit Waghchaure
2f9643d44d refactor: reposting for better peformance
(cherry picked from commit 20787ef5da)
2026-04-07 18:51:17 +05:30
mergify[bot]
0505684d22 fix: sync paid and received amount (backport #53039) (#54107)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix: sync paid and received amount (#53039)
2026-04-07 13:06:02 +00:00
Nishka Gosalia
0b958136be Merge pull request #53996 from aerele/partial-billing-timesheet 2026-04-07 16:11:18 +05:30
Smit Vora
652bd396d4 fix: add v15 compatibility for scrap item 2026-04-07 15:43:43 +05:30
Smit Vora
904ac62830 chore: resolve conflicts 2026-04-07 14:51:35 +05:30
Smit Vora
2fee39017c Merge pull request #53974 from vorasmit/backport-50407-50856 2026-04-07 14:17:55 +05:30
Smit Vora
0b0dccd294 fix: remove unnecessary param, and use value from self
(cherry picked from commit 98dfd64f63)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:49 +00:00
Smit Vora
99df61a0d8 test: enhance tests as per review comments
(cherry picked from commit f13d37fbf9)
2026-04-07 08:47:48 +00:00
Smit Vora
7767659b87 test: maintain sufficient stock for scrap item
(cherry picked from commit b892139342)
2026-04-07 08:47:48 +00:00
Smit Vora
84d5b52483 fix: set bom details on disassembly; abs batch qty
(cherry picked from commit ab1fc22431)
2026-04-07 08:47:48 +00:00
Smit Vora
eee6d7e566 fix: process loss with bom path disassembly
(cherry picked from commit 93ad48bc1b)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2026-04-07 08:47:47 +00:00
Smit Vora
d690a0c6bd fix: validate work order consistency in stock entry
(cherry picked from commit ea392b2009)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:47 +00:00
vorasmit
8f01d12b5e fix: use get_value
(cherry picked from commit a71e8bb116)
2026-04-07 08:47:46 +00:00
vorasmit
44d40795df fix: avg stock entries for disassembly from WO
(cherry picked from commit 71fd18bdf9)
2026-04-07 08:47:46 +00:00
vorasmit
841b507502 fix: manufacture entry with group_by support
(cherry picked from commit 3cf1ce8360)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:46 +00:00
Smit Vora
20f81516cf test: disassembly for scrap / secondary item
(cherry picked from commit a6d41151ff)
2026-04-07 08:47:45 +00:00
Smit Vora
229dc23f97 fix: handle disassembly for secondary / scrap items
(cherry picked from commit 2be8313819)
2026-04-07 08:47:45 +00:00
Smit Vora
f9b1df3572 test: disassembly of items with batch and serial numbers
(cherry picked from commit 1693698fed)
2026-04-07 08:47:44 +00:00
Smit Vora
1063a56251 test: additional items in stock entry considered with disassembly
(cherry picked from commit d32977e3a9)
2026-04-07 08:47:44 +00:00
Smit Vora
75eb5ad584 test: disassemble with source stock entry reference
(cherry picked from commit 6988e2cbbc)
2026-04-07 08:47:44 +00:00
Smit Vora
43c507570b test: disassembly from wo
(cherry picked from commit 342a14d340)
2026-04-07 08:47:43 +00:00
Smit Vora
df049cd277 fix: set serial and batch from source stock entry - on disassemble
(cherry picked from commit 13b019ab8e)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:43 +00:00
Smit Vora
b8ddc2f2b9 fix: correct warehouse preference for disassemble
(cherry picked from commit d3d6b5c660)
2026-04-07 08:47:43 +00:00
Smit Vora
b87b445802 fix: auto-set source_stock_entry
(cherry picked from commit 2e4e8bcaa7)
2026-04-07 08:47:42 +00:00
Smit Vora
e9ce0a41e6 fix: add support to fetch items based on manufacture stock entry; fix how it's done from work order
(cherry picked from commit 1ed0124ad7)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:42 +00:00
Smit Vora
583c7b9819 fix: validate qty that can be disassembled from source stock entry.
(cherry picked from commit 6394dead72)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.py
2026-04-07 08:47:41 +00:00
Smit Vora
ef15c0581d fix: support creating disassembly (without link of WO)
(cherry picked from commit dba82720b6)
2026-04-07 08:47:41 +00:00
Smit Vora
835ae27b38 fix: custom button to disassemble manufactured stock entry with work order
(cherry picked from commit b64f86148c)
2026-04-07 08:47:41 +00:00
Smit Vora
849b2e6ebf fix: set_query for source stock entry
(cherry picked from commit b47dfacb3e)
2026-04-07 08:47:40 +00:00
Smit Vora
44f2e9480d fix: disassembly prompt with source stock entry field
(cherry picked from commit 68e97808c5)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.js
#	erpnext/manufacturing/doctype/work_order/work_order.py
2026-04-07 08:47:40 +00:00
Smit Vora
55ee1dcd04 fix: create source_stock_entry to refer to original manufacturing entry
(cherry picked from commit d4baa9a74a)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.json
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-04-07 08:47:40 +00:00
rohitwaghchaure
c81c1ea869 chore: fix test case 2026-04-07 13:11:44 +05:30
rohitwaghchaure
831ddcd5af Merge pull request #54068 from frappe/mergify/bp/version-15-hotfix/pr-54050
fix: GL entries for different exchange rate in the purchase invoice (backport #54050)
2026-04-07 13:10:40 +05:30
mergify[bot]
bcf59e7171 fix: transactions where update stock is 0 should not create SLEs (backport #54035) (#54076)
* fix: transactions where update stock is 0 should not create SLEs (#54035)

(cherry picked from commit 66780543bd)

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

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-07 06:31:51 +00:00
mergify[bot]
ee812687e6 feat: croatian_address_template (backport #53888) (#54057)
Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com>
2026-04-07 10:26:03 +05:30
mergify[bot]
14085de332 fix: resolve user permission error on status change by updating user … (backport #54033) (#54059)
Co-authored-by: Krishna Shirsath <shirsathkrishna19@gmail.com>
2026-04-07 10:25:32 +05:30
Khushi Rawat
22652f30db Merge pull request #53956 from frappe/mergify/bp/version-15-hotfix/pr-53811
fix: prevent selection of group type customer group in customer master (backport #53811)
2026-04-07 03:16:36 +05:30
khushi8112
7794f3033e fix: validation test for customer group 2026-04-07 02:59:32 +05:30
Poovitha Palanivelu
21805bde1f feat(timesheet): allow partial billing and handled return 2026-04-06 23:04:23 +05:30
rohitwaghchaure
93bfd62725 chore: fix conflicts
Removed redundant calculation of billed quantity and adjusted logic for billed amount based on purchase order.
2026-04-06 17:44:36 +05:30
Rohit Waghchaure
def62cf3fe fix: GL entries for different exchange rate in the purchase invoice
(cherry picked from commit a953709640)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
2026-04-06 11:52:49 +00:00
khushi8112
ea3fcc214b fix(test): use non-group customer group in test setup 2026-04-06 16:09:05 +05:30
diptanilsaha
1146c9550a Merge pull request #54046 from frappe/mergify/bp/version-15-hotfix/pr-54042
fix: skip discount amount validation when not saving (backport #54042)
2026-04-06 13:44:03 +05:30
rohitwaghchaure
c5edeae97e Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-53994 2026-04-06 13:35:08 +05:30
rohitwaghchaure
f855cc89c9 chore: fix conflicts 2026-04-06 13:12:03 +05:30
Sagar Vora
1ffbc399e1 test: add test for discount amount on partial purchase receipt
Co-authored-by: ravibharathi656 <131471282+ravibharathi656@users.noreply.github.com>
(cherry picked from commit 135cb5fd67)
2026-04-06 07:30:43 +00:00
Sagar Vora
13eab9f993 fix: skip discount amount validation when not saving
(cherry picked from commit 0975583388)
2026-04-06 07:30:43 +00:00
khushi8112
97684d3dae fix(test): do not use is_group enabled customer group in test
(cherry picked from commit 75fa2b2277)
2026-04-06 12:57:49 +05:30
khushi8112
7a227e048e fix: prevent selection of group type customer group in customer master
(cherry picked from commit 6068dc959f)
2026-04-06 12:57:49 +05:30
mergify[bot]
af0116cdc5 fix: show current stock qty in Stock Entry PDF (backport #53761) (#54031) 2026-04-06 05:36:09 +00:00
mergify[bot]
a71d32e668 fix: update min date based on transaction_date (backport #53803) (#54024)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix: update min date based on transaction_date (#53803)
2026-04-05 21:16:48 +05:30
mergify[bot]
e33abeef7f fix: remove reference in serial/batch when document is cancelled (backport #53979) (#53988) 2026-04-05 15:39:20 +00:00
mergify[bot]
cb0a548a95 fix(manufacturing): handle null cur_dialog in BOM work order dialog (backport #54011) (#54014) 2026-04-05 12:48:16 +05:30
kavin-114
b57db06100 test(stock): add unit test to update stock queue for return
(cherry picked from commit e537896df8)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
2026-04-04 21:17:08 +00:00
kavin-114
05d6cf5c9a fix(stock): update stock queue in SABE for return entries
(cherry picked from commit 0af8077bcc)
2026-04-04 21:17:08 +00:00
Khushi Rawat
67183ad90c Merge pull request #53995 from khushi8112/fix-depreciation-cancel-test
fix(test): pin posting date in test_depreciation_on_cancel_invoice
2026-04-03 00:58:03 +05:30
khushi8112
7f72189665 fix(test): pin posting date in test_depreciation_on_cancel_invoice 2026-04-03 00:33:17 +05:30
Smit Vora
3c327d5225 fix(ux): refresh grid to correctly persist the state of fields 2026-04-01 08:58:00 +05:30
Rohit Waghchaure
62d58702a0 fix: not able to set operation in work order 2026-04-01 08:57:40 +05:30
Rohit Waghchaure
1d36cb55cd feat: Allow Editing of Items and Quantities in Work Order 2026-04-01 08:55:09 +05:30
mergify[bot]
3fbfad1b9b fix: include rejected qty in tax (purchase receipt) (backport #53624) (#53971)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: include rejected qty in tax (purchase receipt) (#53624)
2026-03-31 16:15:42 +00:00
Frappe PR Bot
2597eaad51 chore(release): Bumped to Version 15.103.1
## [15.103.1](https://github.com/frappe/erpnext/compare/v15.103.0...v15.103.1) (2026-03-31)

### Bug Fixes

* trigger release ([39aaefc](39aaefc202))

### Reverts

* botched backport (backport [#53967](https://github.com/frappe/erpnext/issues/53967)) ([#53968](https://github.com/frappe/erpnext/issues/53968)) ([75344e9](75344e9e82)), closes [#53776](https://github.com/frappe/erpnext/issues/53776) [#53766](https://github.com/frappe/erpnext/issues/53766) [#53767](https://github.com/frappe/erpnext/issues/53767)
2026-03-31 14:30:20 +00:00
Mihir Kandoi
39aaefc202 fix: trigger release 2026-03-31 19:58:36 +05:30
mergify[bot]
75344e9e82 revert: botched backport (backport #53967) (#53968)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix(manufacturing): apply work order status filter in job card (#53776)"
fix(manufacturing): apply work order status filter in job card (backport #53766) (#53767)"
2026-03-31 19:24:30 +05:30
Mihir Kandoi
22774fdf87 revert: botched backport (#53967)
fix(manufacturing): apply work order status filter in job card (#53776)"
fix(manufacturing): apply work order status filter in job card (backport #53766) (#53767)"
2026-03-31 13:52:02 +00:00
mergify[bot]
e159c79766 fix: do not show inv dimension unnecessarily in stock entry (backport #53946) (#53950)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: do not show inv dimension unnecessarily in stock entry (#53946)
2026-03-31 16:29:24 +05:30
mergify[bot]
94fe32f189 chore: remove inter warehouse transfer settings (backport #53860) (#53940)
* chore: remove inter warehouse transfer settings (#53860)

(cherry picked from commit 0696bd2082)

# Conflicts:
#	erpnext/stock/doctype/stock_settings/stock_settings.json

* chore: resolve conflicts

---------

Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-31 06:08:02 +00:00
Frappe PR Bot
d39072a689 chore(release): Bumped to Version 15.103.0
# [15.103.0](https://github.com/frappe/erpnext/compare/v15.102.0...v15.103.0) (2026-03-30)

### Bug Fixes

* **bank_account:** added validation to fetch bank account details using `get_bank_account_details` (backport [#53926](https://github.com/frappe/erpnext/issues/53926)) ([#53929](https://github.com/frappe/erpnext/issues/53929)) ([d16061f](d16061f1bc))
* change shipment parcel dimension fields from Int to Float (backport [#53867](https://github.com/frappe/erpnext/issues/53867)) ([#53872](https://github.com/frappe/erpnext/issues/53872)) ([a21b82b](a21b82b238))
* **contract_template:** restrict `create`, `write` and `delete` access only to `System Manager` (backport [#53787](https://github.com/frappe/erpnext/issues/53787)) ([#53788](https://github.com/frappe/erpnext/issues/53788)) ([d50c727](d50c727f89))
* correct item valuation when "Deduct" is used in Purchase Invoice and Receipt. ([2585287](25852879f6))
* **email_campaign:** prevent unsubscribing entire campaign when email group member unsubscribes ([6151a49](6151a496e7))
* flaky currency exchange test (backport [#53813](https://github.com/frappe/erpnext/issues/53813)) ([#53816](https://github.com/frappe/erpnext/issues/53816)) ([d9cd09b](d9cd09b24a))
* invalid dynamic link filter for address doctype (backport [#53849](https://github.com/frappe/erpnext/issues/53849)) ([#53851](https://github.com/frappe/erpnext/issues/53851)) ([f7536f6](f7536f645b))
* **item_dashboard:** escaping `warehouse`, `item_code`, `stock_uom` and `item_name` on `get_data` (backport [#53904](https://github.com/frappe/erpnext/issues/53904)) ([#53912](https://github.com/frappe/erpnext/issues/53912)) ([db70d2e](db70d2e4df))
* **manufacturing:** apply work order status filter in job card ([#53776](https://github.com/frappe/erpnext/issues/53776)) ([78635eb](78635ebe99))
* **manufacturing:** apply work order status filter in job card (backport [#53766](https://github.com/frappe/erpnext/issues/53766)) ([#53767](https://github.com/frappe/erpnext/issues/53767)) ([d6afb9b](d6afb9b10a))
* **manufacturing:** close work order status when stock reservation is… (backport [#53714](https://github.com/frappe/erpnext/issues/53714)) ([#53720](https://github.com/frappe/erpnext/issues/53720)) ([468ca2b](468ca2bde1))
* **manufacturing:** update condition for base hour rate calculation ([#53777](https://github.com/frappe/erpnext/issues/53777)) ([64956ab](64956ab59c))
* **manufacturing:** update the qty precision (backport [#53874](https://github.com/frappe/erpnext/issues/53874)) ([#53884](https://github.com/frappe/erpnext/issues/53884)) ([46f751e](46f751e403))
* **opening_invoice_creation_tool:** sanitize summary content for dashboard (backport [#53917](https://github.com/frappe/erpnext/issues/53917)) ([#53923](https://github.com/frappe/erpnext/issues/53923)) ([b35a6c2](b35a6c2e73))
* purchase invoice for internal transfers should not require PO (backport [#53791](https://github.com/frappe/erpnext/issues/53791)) ([#53792](https://github.com/frappe/erpnext/issues/53792)) ([0a28fb3](0a28fb3ae1))
* purchase invoice missing item ([bcd56ab](bcd56abb62))
* **stock:** add warehouse filter to pick work order raw materials (backport [#53748](https://github.com/frappe/erpnext/issues/53748)) ([#53897](https://github.com/frappe/erpnext/issues/53897)) ([fffd3a7](fffd3a785c))
* **stock:** handle legacy single sle recon entries ([d09207a](d09207ab82))
* **stock:** update company validation for expense account in lcv ([40c2b3c](40c2b3c0f6))
* **templates:** escape attachment `file_url` and `file_name` in `order.html` and `projects.html` ([7b9f262](7b9f2626f8))
* **templates:** using correct syntax of `include` in `projects.html` ([979c594](979c594e98))
* **test:** enable perpetual inventory ([88c16c8](88c16c8378))
* validate if quantity greater than 0 in item dashboard (backport [#53846](https://github.com/frappe/erpnext/issues/53846)) ([#53847](https://github.com/frappe/erpnext/issues/53847)) ([ddf6eab](ddf6eab013))
* **warehouse_capacity_dashboard:** escaping `warehouse`, `item_code` and `company` on `get_data` (backport [#53894](https://github.com/frappe/erpnext/issues/53894)) ([#53899](https://github.com/frappe/erpnext/issues/53899)) ([1eda22c](1eda22c2bd))

### Features

* **report:** add service start/end date and amount with roll-ups in deferred revenue/expense report ([14088ee](14088ee7ac))
2026-03-30 18:03:28 +00:00
mergify[bot]
efdb004f0b fix(warehouse_capacity_dashboard): removed escape from template (backport #53907) (#53908)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(warehouse_capacity_dashboard): removed `escape` from template (#53907)
2026-03-30 23:33:06 +05:30
diptanilsaha
f4a1f04566 Merge pull request #53916 from frappe/version-15-hotfix 2026-03-30 23:31:54 +05:30
Lakshit Jain
01b8ae3e11 Merge pull request #53931 from frappe/mergify/bp/version-15-hotfix/pr-53406
fix: correct item valuation when "Deduct" is used in Purchase Invoice and Receipt. (backport #53406)
2026-03-30 20:39:14 +05:30
ljain112
b80e10e15e chore: resolve conflicts 2026-03-30 20:22:30 +05:30
ljain112
25852879f6 fix: correct item valuation when "Deduct" is used in Purchase Invoice and Receipt.
(cherry picked from commit e68f149d3a)

# Conflicts:
#	erpnext/controllers/buying_controller.py
2026-03-30 14:24:21 +00:00
mergify[bot]
d16061f1bc fix(bank_account): added validation to fetch bank account details using get_bank_account_details (backport #53926) (#53929)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(bank_account): added validation to fetch bank account details using `get_bank_account_details` (#53926)
2026-03-30 13:42:19 +00:00
mergify[bot]
b35a6c2e73 fix(opening_invoice_creation_tool): sanitize summary content for dashboard (backport #53917) (#53923)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(opening_invoice_creation_tool): sanitize summary content for dashboard (#53917)
2026-03-30 13:40:32 +00:00
rohitwaghchaure
5884b71b0a Merge pull request #53910 from frappe/mergify/bp/version-15-hotfix/pr-53906
fix: purchase invoice missing item (backport #53906)
2026-03-30 18:39:08 +05:30
Rohit Waghchaure
bcd56abb62 fix: purchase invoice missing item
(cherry picked from commit af994c1a22)
2026-03-30 18:24:24 +05:30
mergify[bot]
db70d2e4df fix(item_dashboard): escaping warehouse, item_code, stock_uom and item_name on get_data (backport #53904) (#53912)
* fix(item_dashboard): escaping `warehouse`, `item_code`, `stock_uom` and `item_name` on `get_data` (#53904)

(cherry picked from commit fa5238ba12)

# Conflicts:
#	erpnext/stock/dashboard/item_dashboard.py

* chore: resolve conflict

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-03-30 09:55:57 +00:00
rohitwaghchaure
098cbcde10 Merge pull request #53895 from frappe/mergify/bp/version-15-hotfix/pr-53799
fix(stock): update company validation for expense account in lcv (backport #53799)
2026-03-30 14:45:20 +05:30
mergify[bot]
1eda22c2bd fix(warehouse_capacity_dashboard): escaping warehouse, item_code and company on get_data (backport #53894) (#53899)
* fix(warehouse_capacity_dashboard): escaping `warehouse`, `item_code` and `company` on `get_data` (#53894)

(cherry picked from commit ddeb9775ed)

# Conflicts:
#	erpnext/stock/dashboard/warehouse_capacity_dashboard.py

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-03-30 08:34:37 +00:00
mergify[bot]
fffd3a785c fix(stock): add warehouse filter to pick work order raw materials (backport #53748) (#53897)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): add warehouse filter to pick work order raw materials (#53748)
2026-03-30 08:02:27 +00:00
Sudharsanan11
88c16c8378 fix(test): enable perpetual inventory
(cherry picked from commit 875a2e4947)
2026-03-30 07:34:27 +00:00
Sudharsanan11
40c2b3c0f6 fix(stock): update company validation for expense account in lcv
(cherry picked from commit 913168e8b6)
2026-03-30 07:34:27 +00:00
mergify[bot]
46f751e403 fix(manufacturing): update the qty precision (backport #53874) (#53884)
* fix(manufacturing): update the qty precision (#53874)

(cherry picked from commit f3a794384a)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.py

* chore: resolve conflicts

---------

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-29 16:38:51 +00:00
mergify[bot]
a21b82b238 fix: change shipment parcel dimension fields from Int to Float (backport #53867) (#53872)
* fix: change shipment parcel dimension fields from Int to Float (#53867)

(cherry picked from commit 6badf00313)

# Conflicts:
#	erpnext/stock/doctype/shipment_parcel/shipment_parcel.json
#	erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json

* chore: resolve conflicts

* chore: resole conflicts

---------

Co-authored-by: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-29 07:06:55 +00:00
mergify[bot]
f7536f645b fix: invalid dynamic link filter for address doctype (backport #53849) (#53851) 2026-03-27 12:38:29 +00:00
mergify[bot]
ddf6eab013 fix: validate if quantity greater than 0 in item dashboard (backport #53846) (#53847)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: validate if quantity greater than 0 in item dashboard (#53846)
2026-03-27 16:03:48 +05:30
ruthra kumar
3e4c331962 Merge pull request #53829 from frappe/mergify/bp/version-15-hotfix/pr-53429
feat(report): add service start/end date and amount with roll-ups in deferred revenue/expense report (backport #53429)
2026-03-27 10:53:35 +05:30
ruthra kumar
4b1c1d33b0 Merge pull request #53827 from frappe/mergify/bp/version-15-hotfix/pr-53343
fix(email_campaign): prevent unsubscribing entire campaign when email group member unsubscribes (backport #53343)
2026-03-27 10:43:29 +05:30
Shllokkk
14088ee7ac feat(report): add service start/end date and amount with roll-ups in deferred revenue/expense report
(cherry picked from commit 8e5692d8a3)
2026-03-27 05:00:35 +00:00
Shllokkk
6151a496e7 fix(email_campaign): prevent unsubscribing entire campaign when email group member unsubscribes
(cherry picked from commit 56f597f5ad)
2026-03-27 04:57:30 +00:00
rohitwaghchaure
237915dc03 Merge pull request #53809 from frappe/mergify/bp/version-15-hotfix/pr-53216
fix(stock): handle legacy single sle recon entries (backport #53216)
2026-03-26 18:27:19 +05:30
kavin-114
d09207ab82 fix(stock): handle legacy single sle recon entries
(cherry picked from commit 7e6bbcc3fb)
2026-03-26 18:12:09 +05:30
mergify[bot]
d9cd09b24a fix: flaky currency exchange test (backport #53813) (#53816) 2026-03-26 12:38:36 +00:00
mergify[bot]
0a28fb3ae1 fix: purchase invoice for internal transfers should not require PO (backport #53791) (#53792)
* fix: purchase invoice for internal transfers should not require PO (#53791)

(cherry picked from commit 3f74733942)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-26 03:45:18 +00:00
mergify[bot]
d50c727f89 fix(contract_template): restrict create, write and delete access only to System Manager (backport #53787) (#53788)
* fix(contract_template): restrict `create`, `write` and `delete` access only to `System Manager` (#53787)

(cherry picked from commit e136bfbb61)

# Conflicts:
#	erpnext/crm/doctype/contract_template/contract_template.json

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-03-25 15:05:12 +00:00
diptanilsaha
40c8201302 Merge pull request #53780 from frappe/mergify/bp/version-15-hotfix/pr-53779
fix(template): escape attachment `file_url` and `file_name` and jinja syntax (backport #53779)
2026-03-25 15:20:01 +05:30
diptanilsaha
979c594e98 fix(templates): using correct syntax of include in projects.html
(cherry picked from commit bc6561cdd0)
2026-03-25 09:28:18 +00:00
diptanilsaha
7b9f2626f8 fix(templates): escape attachment file_url and file_name in order.html and projects.html
(cherry picked from commit d9760bbf4f)
2026-03-25 09:28:18 +00:00
Pandiyan P
64956ab59c fix(manufacturing): update condition for base hour rate calculation (#53777) 2026-03-25 13:49:31 +05:30
Pandiyan P
78635ebe99 fix(manufacturing): apply work order status filter in job card (#53776) 2026-03-25 13:24:07 +05:30
mergify[bot]
d6afb9b10a fix(manufacturing): apply work order status filter in job card (backport #53766) (#53767)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(manufacturing): apply work order status filter in job card (#53766)
2026-03-25 11:21:31 +05:30
mergify[bot]
468ca2bde1 fix(manufacturing): close work order status when stock reservation is… (backport #53714) (#53720)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix(manufacturing): close work order status when stock reservation is… (#53714)
2026-03-23 16:28:01 +00:00
Frappe PR Bot
1d14ba1639 chore(release): Bumped to Version 15.102.0
# [15.102.0](https://github.com/frappe/erpnext/compare/v15.101.3...v15.102.0) (2026-03-23)

### Bug Fixes

* Adding validation for operation time in BOM ([7707a79](7707a79d44))
* batch validation for subcontracting receipt ([32c0532](32c0532dec))
* **budget-variance-report:** validate 'budget_against' filter (backport [#53079](https://github.com/frappe/erpnext/issues/53079)) ([#53663](https://github.com/frappe/erpnext/issues/53663)) ([d96590c](d96590c4d9))
* check for `submit` permissions instead of `write` permissions when updating status (backport [#53697](https://github.com/frappe/erpnext/issues/53697)) ([#53702](https://github.com/frappe/erpnext/issues/53702)) ([46e784d](46e784d094))
* check posting_date in args (backport [#53303](https://github.com/frappe/erpnext/issues/53303)) ([#53611](https://github.com/frappe/erpnext/issues/53611)) ([e0f1e75](e0f1e757f3))
* consider returned qty in subcontracting report (backport [#53616](https://github.com/frappe/erpnext/issues/53616)) ([#53620](https://github.com/frappe/erpnext/issues/53620)) ([af86fd3](af86fd3cb4))
* deadlock issue for SLE ([540a854](540a8540d6))
* do not overwrite expense account in stock entry (backport [#53658](https://github.com/frappe/erpnext/issues/53658)) ([#53660](https://github.com/frappe/erpnext/issues/53660)) ([90e4f90](90e4f9026d))
* ignore cost center (backport [#53063](https://github.com/frappe/erpnext/issues/53063)) ([#53613](https://github.com/frappe/erpnext/issues/53613)) ([562f93e](562f93e75c))
* incorrect sle calculation when doc has project ([#53599](https://github.com/frappe/erpnext/issues/53599)) ([7acd435](7acd435835))
* initialize all tax columns to resolve Key error in `item_wise_sales_register` and `item_wise_purchase_register` reports (backport [#53323](https://github.com/frappe/erpnext/issues/53323)) ([#53583](https://github.com/frappe/erpnext/issues/53583)) ([119195c](119195c6fa))
* **manufacturing:** update non-stock item dict (backport [#53689](https://github.com/frappe/erpnext/issues/53689)) ([#53698](https://github.com/frappe/erpnext/issues/53698)) ([c0ce34e](c0ce34e12c))
* merge conflicts ([b3f0e2a](b3f0e2a00d))
* PO should not be required for internal transfers (backport [#53681](https://github.com/frappe/erpnext/issues/53681)) ([#53683](https://github.com/frappe/erpnext/issues/53683)) ([04d74ad](04d74ad6eb))
* python error in manufacture entry if transfer against is job card (backport [#53615](https://github.com/frappe/erpnext/issues/53615)) ([#53617](https://github.com/frappe/erpnext/issues/53617)) ([5a3bc27](5a3bc27e2c))
* **sales_invoice:** using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods ([#53636](https://github.com/frappe/erpnext/issues/53636)) ([f8ab56e](f8ab56ecc9))
* set customer details on customer creation at login ([#53509](https://github.com/frappe/erpnext/issues/53509)) ([4f39dfd](4f39dfd642))
* shipping rule applied twice on non stock items (backport [#53655](https://github.com/frappe/erpnext/issues/53655)) ([#53686](https://github.com/frappe/erpnext/issues/53686)) ([5e767ea](5e767ea595))
* stock queue for SABB ([461bc17](461bc1733f))
* **stock:** add company filter while fetching batches (backport [#53369](https://github.com/frappe/erpnext/issues/53369)) ([#53580](https://github.com/frappe/erpnext/issues/53580)) ([c09c599](c09c5999dc))
* **stock:** fix email error message (backport [#53606](https://github.com/frappe/erpnext/issues/53606)) ([#53632](https://github.com/frappe/erpnext/issues/53632)) ([6ea3d56](6ea3d56972))
* **trends:** added validation for `period_based_on` filter (backport [#53690](https://github.com/frappe/erpnext/issues/53690)) ([#53691](https://github.com/frappe/erpnext/issues/53691)) ([974755b](974755b224))
* validate permission before updating status (backport [#53651](https://github.com/frappe/erpnext/issues/53651)) ([#53652](https://github.com/frappe/erpnext/issues/53652)) ([defa1d4](defa1d4a76))

### Features

* add cost center field to the stock entry accounting dimension tab ([e17b5df](e17b5dfe61))
2026-03-23 14:59:14 +00:00
diptanilsaha
a270c02bb4 Merge pull request #53700 from frappe/version-15-hotfix 2026-03-23 20:27:38 +05:30
rohitwaghchaure
28aa21bf83 Merge pull request #53706 from frappe/mergify/bp/version-15-hotfix/pr-53705
fix: batch validation for subcontracting receipt (backport #53705)
2026-03-23 18:52:47 +05:30
mergify[bot]
119195c6fa fix: initialize all tax columns to resolve Key error in item_wise_sales_register and item_wise_purchase_register reports (backport #53323) (#53583)
Co-authored-by: Lakshit Jain <ljain112@gmail.com>
fix: initialize all tax columns to resolve Key error in `item_wise_sales_register` and `item_wise_purchase_register` reports (#53323)
2026-03-23 18:05:54 +05:30
Rohit Waghchaure
32c0532dec fix: batch validation for subcontracting receipt
(cherry picked from commit b8d201658a)
2026-03-23 11:32:42 +00:00
diptanilsaha
46e784d094 fix: check for submit permissions instead of write permissions when updating status (backport #53697) (#53702) 2026-03-23 16:03:50 +05:30
mergify[bot]
c0ce34e12c fix(manufacturing): update non-stock item dict (backport #53689) (#53698)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix(manufacturing): update non-stock item dict (#53689)
2026-03-23 10:26:29 +00:00
Mihir Kandoi
eaf5494502 chore: linter (#53696) 2026-03-23 10:19:41 +00:00
mergify[bot]
04d74ad6eb fix: PO should not be required for internal transfers (backport #53681) (#53683)
* fix: PO should not be required for internal transfers (#53681)

(cherry picked from commit 5154102468)

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

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-23 09:30:20 +00:00
Nishka Gosalia
2de04b8a46 Merge pull request #53693 from frappe/mergify/bp/version-15-hotfix/pr-53649 2026-03-23 14:46:49 +05:30
mergify[bot]
974755b224 fix(trends): added validation for period_based_on filter (backport #53690) (#53691)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(trends): added validation for `period_based_on` filter (#53690)
2026-03-23 14:42:03 +05:30
mergify[bot]
5e767ea595 fix: shipping rule applied twice on non stock items (backport #53655) (#53686)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: shipping rule applied twice on non stock items (#53655)
2026-03-23 14:38:32 +05:30
nishkagosalia
f5bd85b4dc chore: Adding new argument in status updater to skip qty validation
(cherry picked from commit dcd0509089)
2026-03-23 08:57:35 +00:00
rohitwaghchaure
3fcf6cfef7 Merge pull request #53674 from frappe/mergify/bp/version-15-hotfix/pr-53673
fix: stock queue for SABB (backport #53673)
2026-03-22 13:20:05 +05:30
Rohit Waghchaure
461bc1733f fix: stock queue for SABB
(cherry picked from commit 3fcf308ed8)
2026-03-22 07:30:44 +00:00
rohitwaghchaure
812ca37055 Merge pull request #53669 from frappe/mergify/bp/version-15-hotfix/pr-53638
fix: deadlock issue for SLE (backport #53638)
2026-03-21 14:41:01 +05:30
Rohit Waghchaure
540a8540d6 fix: deadlock issue for SLE
(cherry picked from commit f48b03c6ec)
2026-03-21 08:34:28 +00:00
diptanilsaha
d96590c4d9 fix(budget-variance-report): validate 'budget_against' filter (backport #53079) (#53663) 2026-03-20 15:33:36 +05:30
mergify[bot]
90e4f9026d fix: do not overwrite expense account in stock entry (backport #53658) (#53660)
* fix: do not overwrite expense account in stock entry (#53658)

(cherry picked from commit fa35fbdb8e)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-20 09:41:39 +00:00
mergify[bot]
defa1d4a76 fix: validate permission before updating status (backport #53651) (#53652)
* fix: validate permission before updating status (#53651)

(cherry picked from commit 8e17c722fb)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py
#	erpnext/selling/doctype/sales_order/sales_order.py
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-03-19 20:49:53 +05:30
Nishka Gosalia
ff11429941 Merge pull request #53647 from frappe/mergify/bp/version-15-hotfix/pr-53645
fix: Adding validation for operation time in BOM (backport #53645)
2026-03-19 20:21:20 +05:30
Nishka Gosalia
b3f0e2a00d fix: merge conflicts 2026-03-19 19:54:53 +05:30
nishkagosalia
7707a79d44 fix: Adding validation for operation time in BOM
(cherry picked from commit 7f70e62c30)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/test_job_card.py
2026-03-19 13:00:08 +00:00
Frappe PR Bot
94900cb8b8 chore(release): Bumped to Version 15.101.3
## [15.101.3](https://github.com/frappe/erpnext/compare/v15.101.2...v15.101.3) (2026-03-19)

### Bug Fixes

* **sales_invoice:** using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods ([#53636](https://github.com/frappe/erpnext/issues/53636)) ([65d8a17](65d8a176a6))
2026-03-19 10:07:46 +00:00
diptanilsaha
c1be262357 Merge pull request #53639 from frappe/mergify/bp/version-15/pr-53636
fix(sales_invoice): using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods (backport #53636)
2026-03-19 15:36:17 +05:30
diptanilsaha
65d8a176a6 fix(sales_invoice): using msgprint and removed condition checking for is_created_using_pos to refetch payment methods (#53636)
(cherry picked from commit f8ab56ecc9)
2026-03-19 08:49:49 +00:00
diptanilsaha
f8ab56ecc9 fix(sales_invoice): using msgprint and removed condition checking for is_created_using_pos to refetch payment methods (#53636) 2026-03-19 14:18:48 +05:30
Ravibharathi
488ea7f994 Merge pull request #53628 from frappe/mergify/bp/version-15-hotfix/pr-53509
fix: set customer details on customer creation at login (backport #53509)
2026-03-19 13:59:27 +05:30
mergify[bot]
6ea3d56972 fix(stock): fix email error message (backport #53606) (#53632)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): fix email error message (#53606)
2026-03-19 07:39:30 +00:00
Navin-S-R
e2c8dc5386 chore: resolve conflict 2026-03-19 13:06:16 +05:30
mergify[bot]
9c243e8dd0 refactor: remove test file import in stock ageing report (backport #53619) (#53625)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-19 06:50:54 +00:00
Sakthivel Murugan S
4f39dfd642 fix: set customer details on customer creation at login (#53509)
(cherry picked from commit 256d267a3b)

# Conflicts:
#	erpnext/portal/utils.py
2026-03-19 06:34:50 +00:00
mergify[bot]
af86fd3cb4 fix: consider returned qty in subcontracting report (backport #53616) (#53620)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consider returned qty in subcontracting report (#53616)
2026-03-19 11:52:44 +05:30
mergify[bot]
5a3bc27e2c fix: python error in manufacture entry if transfer against is job card (backport #53615) (#53617)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: python error in manufacture entry if transfer against is job card (#53615)
2026-03-19 05:13:36 +00:00
mergify[bot]
562f93e75c fix: ignore cost center (backport #53063) (#53613)
Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com>
fix: ignore cost center (#53063)
2026-03-19 04:41:10 +00:00
mergify[bot]
e0f1e757f3 fix: check posting_date in args (backport #53303) (#53611)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix: check posting_date in args (#53303)
2026-03-19 04:38:20 +00:00
rohitwaghchaure
7b64f88734 Merge pull request #53600 from frappe/mergify/bp/version-15-hotfix/pr-53599
fix: incorrect sle calculation when doc has project (backport #53599)
2026-03-18 19:07:26 +05:30
Mihir Kandoi
7acd435835 fix: incorrect sle calculation when doc has project (#53599)
(cherry picked from commit 6cb6a52ded)
2026-03-18 13:20:17 +00:00
rohitwaghchaure
16fe458b92 Merge pull request #53585 from frappe/mergify/bp/version-15-hotfix/pr-53246
feat: add cost center field to the stock entry accounting dimension tab (backport #53246)
2026-03-18 16:52:27 +05:30
rohitwaghchaure
4c2dba98da chore: fix conflicts
Removed several fields related to additional transfer entries and subcontracting inward orders from the stock entry JSON.
2026-03-18 16:32:56 +05:30
sudarshan-g
e17b5dfe61 feat: add cost center field to the stock entry accounting dimension tab
(cherry picked from commit 47772f4e77)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.json
2026-03-18 06:20:08 +00:00
mergify[bot]
c09c5999dc fix(stock): add company filter while fetching batches (backport #53369) (#53580)
* fix(stock): add company filter while fetching batches (#53369)

(cherry picked from commit 31d14df37b)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.py
#	erpnext/stock/doctype/pick_list/pick_list.py
#	erpnext/stock/doctype/pick_list_item/pick_list_item.json

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-18 05:04:52 +00:00
mergify[bot]
a7bf55b4bf chore: make supplier data expanded by default in PI (backport #53565) (#53578)
* chore: make supplier data expanded by default in PI (#53565)

(cherry picked from commit b433852f8a)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-18 04:59:04 +00:00
206 changed files with 6079 additions and 2144 deletions

View File

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

View File

@@ -34,6 +34,13 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -317,13 +324,21 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
"account_type": "Expense Account"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -340,7 +355,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_type": "Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -0,0 +1,840 @@
{
"name": "Philippines",
"country": "Philippines",
"tree": {
"Asset": {
"account_number": "1000",
"is_group": 1,
"root_type": "Asset",
"Current Assets": {
"account_number": "1001",
"is_group": 1,
"root_type": "Asset",
"Cash": {
"account_number": "1100",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Cash on Hand": {
"account_number": "1101",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
}
}
},
"Bank Accounts": {
"account_number": "1102",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
},
"Advances to Officers & Employees": {
"account_number": "1290",
"is_group": 1,
"root_type": "Asset",
"Advances to Officers & Employees": {
"account_number": "1291",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable Trade": {
"account_number": "1300",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Trade": {
"account_number": "1301",
"is_group": 0,
"root_type": "Asset",
"account_type": "Receivable"
}
},
"Accounts Receivable - Affiliates": {
"account_number": "1310",
"is_group": 1,
"root_type": "Asset",
"Due from Company": {
"account_number": "1311",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable - Others": {
"account_number": "1400",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Others": {
"account_number": "1401",
"is_group": 0,
"root_type": "Asset"
}
},
"Parts, Materials and Supplies": {
"account_number": "1500",
"is_group": 1,
"root_type": "Asset",
"Parts, Materials and Supplies": {
"account_number": "1501",
"is_group": 0,
"root_type": "Asset"
},
"Raw Materials - Demo": {
"account_number": "1502",
"is_group": 0,
"root_type": "Asset"
}
},
"Project in Progress": {
"account_number": "1510",
"is_group": 1,
"root_type": "Asset",
"Project in Progress": {
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
}
},
"Prepayments": {
"account_number": "1600",
"is_group": 1,
"root_type": "Asset",
"Prepaid Insurance & Bonds": {
"account_number": "1601",
"is_group": 0,
"root_type": "Asset"
},
"Prepaid Rent": {
"account_number": "1602",
"is_group": 0,
"root_type": "Asset"
}
},
"VAT Input Tax": {
"account_number": "1610",
"is_group": 1,
"root_type": "Asset",
"VAT Input Tax - Goods": {
"account_number": "1611",
"is_group": 0,
"root_type": "Asset",
"account_type": "Tax"
}
}
},
"Non - Current Assets": {
"account_number": "1002",
"is_group": 1,
"root_type": "Asset",
"Property, Plants And Equipments": {
"account_number": "1700",
"is_group": 1,
"root_type": "Asset",
"Land": {
"account_number": "1701",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Buildings & Improvements": {
"account_number": "1702",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Delivery & Trans Equipment": {
"account_number": "1703",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Furniture & Fixtures": {
"account_number": "1704",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Machinery & Equipment": {
"account_number": "1705",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
}
},
"Accum Depr. - Property, Plants and Equipment": {
"account_number": "1800",
"is_group": 1,
"root_type": "Asset",
"Accumulated Dep Bdgs & Improv": {
"account_number": "1801",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Delivery & Trans": {
"account_number": "1802",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Furniture & Fixture": {
"account_number": "1803",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Depreciation - Machinery & Equipment": {
"account_number": "1804",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
},
"Other Assets": {
"account_number": "1003",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1900",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1901",
"is_group": 0,
"root_type": "Asset"
}
},
"Miscellaneous Deposits": {
"account_number": "1910",
"is_group": 1,
"root_type": "Asset",
"Miscellaneous Deposits": {
"account_number": "1911",
"is_group": 0,
"root_type": "Asset"
}
},
"Retirement Fund": {
"account_number": "1920",
"is_group": 1,
"root_type": "Asset",
"Retirement Fund": {
"account_number": "1921",
"is_group": 0,
"root_type": "Asset"
}
},
"Investment": {
"account_number": "1930",
"is_group": 1,
"root_type": "Asset",
"Investment": {
"account_number": "1931",
"is_group": 0,
"root_type": "Asset"
}
},
"System Development": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
"System Development": {
"account_number": "1941",
"is_group": 0,
"root_type": "Asset"
}
}
}
},
"Liability": {
"account_number": "2000",
"is_group": 1,
"root_type": "Liability",
"Current Liabilities": {
"account_number": "2001",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable Trade": {
"account_number": "2100",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Trade": {
"account_number": "2101",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Accounts Payable Others": {
"account_number": "2110",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Payroll": {
"account_number": "2111",
"is_group": 0,
"root_type": "Liability"
}
},
"VAT Output Tax": {
"account_number": "2200",
"is_group": 1,
"root_type": "Liability",
"VAT Output Tax": {
"account_number": "2201",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Wages": {
"account_number": "2210",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Wages": {
"account_number": "2211",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Expanded": {
"account_number": "2220",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Expanded": {
"account_number": "2221",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Accruals And Other Current Payables": {
"account_number": "2300",
"is_group": 1,
"root_type": "Liability",
"Stock Received But Not Billed": {
"account_number": "2301",
"is_group": 0,
"root_type": "Liability",
"account_type": "Stock Received But Not Billed"
}
},
"Payable to Government and Other Institutions": {
"account_number": "2400",
"is_group": 1,
"root_type": "Liability",
"SSS Premium Payable": {
"account_number": "2401",
"is_group": 0,
"root_type": "Liability"
},
"SSS Salary Loan Payable": {
"account_number": "2402",
"is_group": 0,
"root_type": "Liability"
},
"PhilHealth Premium": {
"account_number": "2403",
"is_group": 0,
"root_type": "Liability"
},
"Pag-ibig Loan Payable": {
"account_number": "2404",
"is_group": 0,
"root_type": "Liability"
},
"Coop Loans": {
"account_number": "2405",
"is_group": 0,
"root_type": "Liability"
},
"Coop Contributions": {
"account_number": "2406",
"is_group": 0,
"root_type": "Liability"
},
"Canteen": {
"account_number": "2407",
"is_group": 0,
"root_type": "Liability"
},
"AUB Loan Payable": {
"account_number": "2408",
"is_group": 0,
"root_type": "Liability"
},
"HSBC Loan Payable": {
"account_number": "2409",
"is_group": 0,
"root_type": "Liability"
}
},
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Non Current Liabilities": {
"account_number": "2002",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2600",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2601",
"is_group": 0,
"root_type": "Liability"
}
},
"Deferred Income": {
"account_number": "2700",
"is_group": 1,
"root_type": "Liability",
"Deferred Income": {
"account_number": "2701",
"is_group": 0,
"root_type": "Liability"
}
},
"Notes Payable": {
"account_number": "2800",
"is_group": 1,
"root_type": "Liability",
"Notes Payable": {
"account_number": "2801",
"is_group": 0,
"root_type": "Liability"
}
},
"Dividends Payable": {
"account_number": "2900",
"is_group": 0,
"root_type": "Liability"
}
}
},
"Equity": {
"account_number": "3000",
"is_group": 1,
"root_type": "Equity",
"STOCKHOLDER'S EQUITY": {
"account_number": "3001",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3100",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3101",
"is_group": 0,
"root_type": "Equity"
}
},
"Subscription Receivable": {
"account_number": "3200",
"is_group": 1,
"root_type": "Equity",
"Subscription Receivable": {
"account_number": "3201",
"is_group": 0,
"root_type": "Equity"
}
},
"Retained Earnings": {
"account_number": "3300",
"is_group": 1,
"root_type": "Equity",
"Retained Earnings": {
"account_number": "3301",
"is_group": 0,
"root_type": "Equity"
}
},
"Current Year (Profit/Loss)": {
"account_number": "3400",
"is_group": 0,
"root_type": "Equity"
},
"Drawings": {
"account_number": "3500",
"is_group": 1,
"root_type": "Equity",
"Drawings": {
"account_number": "3501",
"is_group": 0,
"root_type": "Equity"
}
}
}
},
"Income": {
"account_number": "4000",
"is_group": 1,
"root_type": "Income",
"Gross Sales": {
"account_number": "4100",
"is_group": 1,
"root_type": "Income",
"Sales": {
"account_number": "4101",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Adjustment": {
"account_number": "4200",
"is_group": 1,
"root_type": "Income",
"Sales Return And Allowance": {
"account_number": "4201",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Discount": {
"account_number": "4300",
"is_group": 1,
"root_type": "Income",
"Sales Discount": {
"account_number": "4301",
"is_group": 0,
"root_type": "Income"
}
},
"Other Income": {
"account_number": "6000",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6010",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6011",
"is_group": 0,
"root_type": "Income"
}
},
"Dividend Income": {
"account_number": "6020",
"is_group": 1,
"root_type": "Income",
"Dividend Income": {
"account_number": "6021",
"is_group": 0,
"root_type": "Income"
}
}
}
},
"Expense": {
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,
"root_type": "Expense",
"Salaries, Wages": {
"account_number": "5101",
"is_group": 0,
"root_type": "Expense"
},
"13th Month Pay & Bonus": {
"account_number": "5102",
"is_group": 0,
"root_type": "Expense"
},
"Overtime & Night Diff": {
"account_number": "5103",
"is_group": 0,
"root_type": "Expense"
},
"Incentive/Performance Bonus": {
"account_number": "5104",
"is_group": 0,
"root_type": "Expense"
},
"Employees Benefits": {
"account_number": "5105",
"is_group": 0,
"root_type": "Expense"
},
"Advertising & Promotions": {
"account_number": "5106",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Leasehold Improvement": {
"account_number": "5107",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Pre-Operating": {
"account_number": "5108",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of System Development": {
"account_number": "5109",
"is_group": 0,
"root_type": "Expense"
},
"Audit & Legal Fee": {
"account_number": "5110",
"is_group": 0,
"root_type": "Expense"
},
"Bad Debts Expenses": {
"account_number": "5111",
"is_group": 0,
"root_type": "Expense"
},
"Client Service & Maintenance": {
"account_number": "5112",
"is_group": 0,
"root_type": "Expense"
},
"Commission Expenses": {
"account_number": "5113",
"is_group": 0,
"root_type": "Expense"
},
"Communications": {
"account_number": "5114",
"is_group": 0,
"root_type": "Expense"
},
"Contractual Services": {
"account_number": "5115",
"is_group": 0,
"root_type": "Expense"
},
"Depreciation Expenses": {
"account_number": "5116",
"is_group": 0,
"root_type": "Expense",
"account_type": "Depreciation"
},
"Donation & Contribution": {
"account_number": "5117",
"is_group": 0,
"root_type": "Expense"
},
"Dues & Subscription": {
"account_number": "5118",
"is_group": 0,
"root_type": "Expense"
},
"Employee Med/Dental/Hosp Expenses": {
"account_number": "5119",
"is_group": 0,
"root_type": "Expense"
},
"Employee Uniforms": {
"account_number": "5120",
"is_group": 0,
"root_type": "Expense"
},
"Equipage": {
"account_number": "5121",
"is_group": 0,
"root_type": "Expense"
},
"Expenses for Reclassification": {
"account_number": "5122",
"is_group": 0,
"root_type": "Expense"
},
"Gas & Oil": {
"account_number": "5123",
"is_group": 0,
"root_type": "Expense"
},
"Insurance Expenses": {
"account_number": "5124",
"is_group": 0,
"root_type": "Expense"
},
"Light & Water": {
"account_number": "5125",
"is_group": 0,
"root_type": "Expense"
},
"Local/Overseas Travel": {
"account_number": "5126",
"is_group": 0,
"root_type": "Expense"
},
"Meals & Transportation Expenses": {
"account_number": "5127",
"is_group": 0,
"root_type": "Expense"
},
"Meeting & Conferences": {
"account_number": "5128",
"is_group": 0,
"root_type": "Expense"
},
"Miscellaneous Expenses": {
"account_number": "5129",
"is_group": 0,
"root_type": "Expense"
},
"Mockup Expenses": {
"account_number": "5130",
"is_group": 0,
"root_type": "Expense"
},
"Obsolescence Expenses": {
"account_number": "5131",
"is_group": 0,
"root_type": "Expense"
},
"Other Support Cost": {
"account_number": "5132",
"is_group": 0,
"root_type": "Expense"
},
"Pag-ibig Contribution": {
"account_number": "5133",
"is_group": 0,
"root_type": "Expense"
},
"Performance Bonds": {
"account_number": "5134",
"is_group": 0,
"root_type": "Expense"
},
"Pre Employment Expenses": {
"account_number": "5135",
"is_group": 0,
"root_type": "Expense"
},
"Professional Fees": {
"account_number": "5136",
"is_group": 0,
"root_type": "Expense"
},
"Recruitment & Employment": {
"account_number": "5137",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses": {
"account_number": "5138",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses Others": {
"account_number": "5139",
"is_group": 0,
"root_type": "Expense"
},
"Repairs & Maintenance": {
"account_number": "5140",
"is_group": 0,
"root_type": "Expense"
},
"Representation Expenses": {
"account_number": "5141",
"is_group": 0,
"root_type": "Expense"
},
"Research & Development": {
"account_number": "5142",
"is_group": 0,
"root_type": "Expense"
},
"Security Expenses": {
"account_number": "5143",
"is_group": 0,
"root_type": "Expense"
},
"Shared Services Fee": {
"account_number": "5144",
"is_group": 0,
"root_type": "Expense"
},
"SSS/Medicare/EC Contributions": {
"account_number": "5145",
"is_group": 0,
"root_type": "Expense"
},
"Stationery & Supplies": {
"account_number": "5146",
"is_group": 0,
"root_type": "Expense"
},
"Taxes & Licenses": {
"account_number": "5147",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Training & Seminar": {
"account_number": "5148",
"is_group": 0,
"root_type": "Expense"
}
},
"Stock Adjustment": {
"account_number": "5200",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"
}
}
}
}

View File

@@ -82,13 +82,15 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self):
def on_update(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def on_trash(self):
if frappe.flags.in_test:
@@ -103,10 +105,6 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:

View File

@@ -30,16 +30,6 @@ frappe.ui.form.on("Accounts Settings", {
add_taxes_from_item_tax_template(frm) {
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
},
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
});
function toggle_tax_settings(frm, field_name) {

View File

@@ -95,7 +95,6 @@
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings",
@@ -561,7 +560,7 @@
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -623,13 +622,6 @@
"fieldname": "column_break_ntmi",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
},
{
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
@@ -671,7 +663,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-06 14:49:11.467716",
"modified": "2026-05-18 12:16:33.679345",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -701,4 +693,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -60,7 +60,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
@@ -170,11 +170,3 @@ class AccountsSettings(Document):
),
title=_("Auto Tax Settings Error"),
)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

@@ -131,6 +131,7 @@ def get_default_company_bank_account(company, party_type, party):
@frappe.whitelist()
def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

@@ -398,7 +398,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_group": "All Customer Groups",
"customer_group": "Individual",
"customer_type": "Company",
"customer_name": "Poore Simon's",
}
@@ -429,7 +429,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_group": "All Customer Groups",
"customer_group": "Individual",
"customer_type": "Company",
"customer_name": "Fayva",
}

View File

@@ -1,108 +1,41 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-11-22 23:47:02.804568",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-11-22 23:47:02.804568",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "tax_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-21 23:51:39.445198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"istable": 1,
"links": [],
"modified": "2026-04-30 23:49:27.020639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import flt, nowdate
from frappe.utils import escape_html, flt, nowdate
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -84,6 +84,11 @@ class OpeningInvoiceCreationTool(Document):
)
prepare_invoice_summary(doctype, invoices)
invoices_summary_companies = list(invoices_summary.keys())
for company in invoices_summary_companies:
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
return invoices_summary, max_count
def validate_company(self):

View File

@@ -209,7 +209,7 @@ def make_customer(customer=None):
{
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_group": "Individual",
"customer_type": "Company",
"territory": "All Territories",
}

View File

@@ -725,31 +725,12 @@ frappe.ui.form.on("Payment Entry", {
if (!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from) {
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: frm.doc.paid_from_account_currency,
to_currency: company_currency,
transaction_date: frm.doc.posting_date,
},
callback: function (r, rt) {
frm.set_value("source_exchange_rate", r.message);
},
});
} else {
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_currency
);
}
}
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_currency
);
},
paid_to_account_currency: function (frm) {
@@ -781,49 +762,24 @@ frappe.ui.form.on("Payment Entry", {
posting_date: function (frm) {
frm.events.paid_from_account_currency(frm);
frm.events.paid_to_account_currency(frm);
},
source_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
// target exchange rate should always be same as source if both account currencies is same
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
target_exchange_rate: function (frm) {
frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.received_amount) {
frm.set_value(
"base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (
!frm.doc.source_exchange_rate &&
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("paid_amount", frm.doc.base_received_amount);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
// target exchange rate should always be same as source if both account currencies is same
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
} else {
frm.set_value(
"paid_amount",
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
);
}
// set_unallocated_amount is called by below method,
@@ -832,6 +788,32 @@ frappe.ui.form.on("Payment Entry", {
}
frm.set_paid_amount_based_on_received_amount = false;
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
target_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (
!frm.doc.source_exchange_rate &&
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
} else {
frm.set_value(
"received_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},

View File

@@ -350,7 +350,7 @@
"reqd": 1
},
{
"depends_on": "doc.received_amount",
"depends_on": "eval:doc.received_amount;",
"fieldname": "base_received_amount",
"fieldtype": "Currency",
"label": "Received Amount (Company Currency)",
@@ -800,7 +800,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-05-15 18:01:04.013025",
"modified": "2026-05-15 13:31:01.166010",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2330,18 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
condition += f" and {fieldname} between {from_date} and {to_date}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
condition += f" and {fieldname} >= {from_date}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
condition += f" and {fieldname} <= {to_date}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
@@ -2561,7 +2560,7 @@ def get_orders_to_be_billed(
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"

View File

@@ -200,6 +200,30 @@ class TestPaymentEntry(FrappeTestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -1937,6 +1961,37 @@ class TestPaymentEntry(FrappeTestCase):
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
@@ -2043,6 +2098,7 @@ def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
customer = customer.name
return customer

View File

@@ -80,6 +80,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
self.customer = customer.name

View File

@@ -2546,6 +2546,7 @@ def make_customer(customer_name, currency=None):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
customer.customer_group = "Individual"
if currency:
customer.default_currency = currency

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
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"),
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -812,6 +812,7 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -858,7 +859,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-05-07 15:56:54.343317",
"modified": "2026-04-20 16:16:12.322024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -243,8 +243,10 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
date = (
args.get("transaction_date")
or args.get("posting_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')
@@ -656,7 +658,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.is_recursive:
transaction_qty = sum(
[
row.qty
flt(row.qty)
for row in doc.items
if not row.is_free_item
and row.item_code == args.item_code

View File

@@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", {
selling: function (frm) {
frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("buying", !frm.doc.selling);
},
buying: function (frm) {
frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("selling", !frm.doc.buying);
},
set_options_for_applicable_for: function (frm) {

View File

@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -126,7 +131,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -460,13 +471,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
on_submit() {
@@ -575,12 +587,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -382,7 +382,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "bill_no",
"collapsible_depends_on": "posting_date",
"fieldname": "supplier_invoice_details",
"fieldtype": "Section Break",
"label": "Supplier Invoice"
@@ -1660,7 +1660,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-02-05 20:45:16.964500",
"modified": "2026-03-17 20:44:00.221219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -612,12 +612,13 @@ class PurchaseInvoice(BuyingController):
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
if frappe.get_value(
if (
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
and not self.is_internal_transfer()
and not frappe.db.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
):
return
)
):
for d in self.get("items"):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
@@ -977,6 +978,10 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts()
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
@@ -1145,7 +1150,11 @@ class PurchaseInvoice(BuyingController):
)
# check if the exchange rate has changed
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
if (
not adjust_incoming_rate
and item.get("purchase_receipt")
and self.auto_accounting_for_stock
):
if (
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
@@ -1182,6 +1191,7 @@ class PurchaseInvoice(BuyingController):
item=item,
)
)
if (
self.auto_accounting_for_stock
and self.is_opening == "No"

View File

@@ -356,6 +356,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
@@ -376,12 +382,17 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[0].base_net_amount - pr.items[0].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,

View File

@@ -109,6 +109,7 @@
"sales_invoice_item",
"material_request",
"material_request_item",
"delivered_by_supplier",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -731,7 +732,6 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -979,12 +979,21 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"hidden": 1,
"label": "Delivered by Supplier",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 13:01:54.441511",
"modified": "2026-05-06 08:08:40.782395",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -31,6 +31,7 @@ class PurchaseInvoiceItem(Document):
conversion_factor: DF.Float
cost_center: DF.Link | None
deferred_expense_account: DF.Link | None
delivered_by_supplier: DF.Check
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -136,13 +136,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
cur_frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
}
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -166,13 +168,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
);
}
}
// Show buttons only when pos view is active
if (cint(doc.docstatus == 0) && cur_frm.page.current_view_name !== "pos" && !doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.delivery_note_btn();
this.frm.cscript.quotation_btn();
}
this.toggle_get_items();
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
@@ -258,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
}
toggle_get_items() {
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
buttons.forEach((label) => {
this.frm.remove_custom_button(label, "Get Items From");
});
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
return;
}
if (!this.frm.doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.quotation_btn();
this.frm.cscript.timesheet_btn();
}
this.frm.cscript.delivery_note_btn();
}
timesheet_btn() {
var me = this;
me.frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: me.frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: me.frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
me.frm.events.add_timesheet_data(me.frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
sales_order_btn() {
var me = this;
this.$sales_order_btn = this.frm.add_custom_button(
@@ -322,6 +405,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note",
@@ -334,7 +423,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = {
docstatus: 1,
company: me.frm.doc.company,
is_return: 0,
is_return: me.frm.doc.is_return,
};
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return {
@@ -453,12 +542,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
set_dynamic_labels() {
@@ -594,6 +685,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.calculate_taxes_and_totals();
}
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
is_return() {
this.toggle_get_items();
}
};
// for backward compatibility: combine new and previous states
@@ -1039,71 +1138,6 @@ frappe.ui.form.on("Sales Invoice", {
},
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}

View File

@@ -777,8 +777,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval:!doc.is_return",
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -792,7 +791,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2112,7 +2110,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"fieldname": "section_break_104",
"fieldtype": "Section Break"
},
@@ -2200,7 +2198,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-02-05 20:43:44.732805",
"modified": "2026-04-06 22:30:28.513139",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -323,10 +323,22 @@ class SalesInvoice(SellingController):
)
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
if self.is_return and not self.return_against and self.timesheets:
frappe.throw(_("Direct return is not allowed for Timesheet."))
if not self.is_return:
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if self.is_return:
self.timesheets = []
if self.is_return and self.return_against:
for row in self.timesheets:
if row.billing_hours:
row.billing_hours = -abs(row.billing_hours)
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -494,7 +506,7 @@ class SalesInvoice(SellingController):
if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_sheet(self.name)
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company)
@@ -550,7 +562,7 @@ class SalesInvoice(SellingController):
self.check_if_consolidated_invoice()
super().before_cancel()
self.update_time_sheet(None)
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -735,8 +747,20 @@ class SalesInvoice(SellingController):
for data in timesheet.time_logs:
if (
(self.project and args.timesheet_detail == data.name)
or (not self.project and not data.sales_invoice)
or (not sales_invoice and data.sales_invoice == self.name)
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == self.name
and args.timesheet_detail == data.name
)
or (
self.is_return
and self.return_against
and data.sales_invoice
and data.sales_invoice == self.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
):
data.sales_invoice = sales_invoice
@@ -776,11 +800,25 @@ class SalesInvoice(SellingController):
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
# Note: This validation is skipped for return invoices
# to allow returns to reference already-billed timesheet details
for data in self.timesheets:
# Handle invoice duplication
if data.time_sheet and data.timesheet_detail:
if sales_invoice := frappe.db.get_value(
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
):
frappe.throw(
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
)
)
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
if status not in ["Submitted", "Payslip", "Partially Billed"]:
frappe.throw(
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
)
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@@ -1112,7 +1150,12 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
if (
not self.is_return
and not self.timesheets
and self.project
and self.is_auto_fetch_timesheet_enabled()
):
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@@ -2516,7 +2559,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
},
doctype + " Item": item_field_map,
},
@@ -2745,7 +2788,7 @@ def update_multi_mode_option(doc, pos_profile):
payment.account = payment_mode.default_account
payment.type = payment_mode.type
mop_refetched = bool(doc.payments) and not doc.is_created_using_pos
mop_refetched = bool(doc.payments)
doc.set("payments", [])
invalid_modes = []
@@ -2769,9 +2812,8 @@ def update_multi_mode_option(doc, pos_profile):
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
if mop_refetched:
frappe.toast(
_("Payment methods refreshed. Please review before proceeding."),
indicator="orange",
frappe.msgprint(
_("Payment methods refreshed. Please review before proceeding."), indicator="orange", alert=True
)

View File

@@ -2917,7 +2917,7 @@ class TestSalesInvoice(FrappeTestCase):
si.submit()
# Check if adjustment entry is created
self.assertTrue(
self.assertFalse(
frappe.db.exists(
"GL Entry",
{
@@ -3230,7 +3230,7 @@ class TestSalesInvoice(FrappeTestCase):
calculate_depreciation=1,
submit=1,
)
post_depreciation_entries()
post_depreciation_entries(date="2025-04-01")
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=10000, posting_date=getdate("2025-05-01")
@@ -4835,6 +4835,33 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def test_inter_company_transaction_cost_center(self):
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
parent_cost_center="Main - WP",
cost_center="Main - WP",
currency="USD",
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
cost_center = frappe.db.get_value("Company", "_Test Company 1", "cost_center")
frappe.db.set_value("Company", "_Test Company 1", "cost_center", None)
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
self.assertEqual(target_doc.cost_center, None)
self.assertEqual(target_doc.items[0].cost_center, None)
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -52,7 +52,6 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -117,7 +116,7 @@
],
"istable": 1,
"links": [],
"modified": "2021-10-02 03:48:44.979777",
"modified": "2026-04-06 22:30:28.513139",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",

View File

@@ -25,6 +25,10 @@ frappe.ui.form.on("Shipping Rule", {
},
calculate_based_on: function (frm) {
frm.trigger("toggle_reqd");
if (frm.doc.calculate_based_on === "Fixed") {
frm.clear_table("conditions");
frm.refresh_field("conditions");
}
},
toggle_reqd: function (frm) {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");

View File

@@ -58,6 +58,11 @@ class ShippingRule(Document):
self.validate_overlapping_shipping_rule_conditions()
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:
self.set("conditions", [])
return
zero_to_values = []
for d in self.get("conditions"):
@@ -152,7 +157,9 @@ class ShippingRule(Document):
frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges"
shipping_charge["category"] = "Valuation and Total"
shipping_charge["category"] = (
"Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total"
)
shipping_charge["add_deduct_tax"] = "Add"
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)

View File

@@ -629,18 +629,21 @@ def create_parties():
customer.customer_name = "_Test Subscription Customer"
customer.default_currency = "USD"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
customer.customer_group = "Individual"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
customer = frappe.new_doc("Customer")
customer.customer_name = "Test Subscription Customer Multi Currency"
customer.default_currency = "USD"
customer.customer_group = "Individual"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer John Doe"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable - _TC"})
customer.customer_group = "Individual"
customer.insert()

View File

@@ -669,7 +669,7 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
if not default_due_date:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
if getdate(default_due_date) != getdate(posting_date) and getdate(due_date) > getdate(default_due_date):
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"

View File

@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -120,3 +120,49 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -129,8 +129,6 @@ class ReceivablePayableReport:
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
elif self.ple_fetch_method == "Raw SQL":
self.fetch_ple_in_sql_procedures()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
@@ -194,6 +192,7 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -320,79 +319,6 @@ class ReceivablePayableReport:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
def fetch_ple_in_sql_procedures(self):
self.proc = InitSQLProceduresForAR()
build_balance = f"""
begin not atomic
declare done boolean default false;
declare rec1 row type of `{self.proc._row_def_table_name}`;
declare ple cursor for {self.ple_query.get_sql()};
declare continue handler for not found set done = true;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.init_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
set done = false;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.allocate_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
end;
"""
frappe.db.sql(build_balance)
balances = frappe.db.sql(
f"""select
name,
voucher_type,
voucher_no,
party,
party_account `account`,
posting_date,
account_currency,
cost_center,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
sum(paid_in_account_currency) `paid_in_account_currency`,
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
as_dict=True,
)
for x in balances:
if self.filters.get("ignore_accounts"):
key = (x.voucher_type, x.voucher_no, x.party)
else:
key = (x.account, x.voucher_type, x.voucher_no, x.party)
_d = self.build_voucher_dict(x)
for field in [
"invoiced",
"paid",
"credit_note",
"outstanding",
"invoiced_in_account_currency",
"paid_in_account_currency",
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
]:
_d[field] = x.get(field)
self.voucher_balance[key] = _d
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@@ -925,6 +851,7 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -992,6 +919,9 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1231,6 +1161,7 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1382,134 +1313,3 @@ def get_party_group_with_children(party, party_groups):
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_party_groups))
class InitSQLProceduresForAR:
"""
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
"""
_varchar_type = get_definition("Data")
_currency_type = get_definition("Currency")
# Temporary Tables
_voucher_balance_name = "_ar_voucher_balance"
_voucher_balance_definition = f"""
create temporary table `{_voucher_balance_name}`(
name {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
party {_varchar_type},
party_account {_varchar_type},
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
invoiced_in_account_currency {_currency_type},
paid_in_account_currency {_currency_type},
credit_note_in_account_currency {_currency_type}) engine=memory;
"""
_row_def_table_name = "_ar_ple_row"
_row_def_table_definition = f"""
create temporary table `{_row_def_table_name}`(
name {_varchar_type},
account {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
against_voucher_type {_varchar_type},
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
account_currency {_varchar_type},
amount {_currency_type},
amount_in_account_currency {_currency_type}) engine=memory;
"""
# Function
genkey_function_name = "ar_genkey"
genkey_function_sql = f"""
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
begin
if allocate then
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
else
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
end if;
end
"""
# Procedures
init_procedure_name = "ar_init_tmp_table"
init_procedure_sql = f"""
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
then
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
allocate_procedure_name = "ar_allocate_to_tmp_table"
allocate_procedure_sql = f"""
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
declare invoiced {_currency_type} default 0;
declare invoiced_in_account_currency {_currency_type} default 0;
declare paid {_currency_type} default 0;
declare paid_in_account_currency {_currency_type} default 0;
declare credit_note {_currency_type} default 0;
declare credit_note_in_account_currency {_currency_type} default 0;
if ple.amount > 0 then
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set invoiced = ple.amount;
set invoiced_in_account_currency = ple.amount_in_account_currency;
end if;
else
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
if (ple.voucher_no = ple.against_voucher_no) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set credit_note = -1 * ple.amount;
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
else
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
end if;
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""
def __init__(self):
existing_procedures = frappe.db.get_routines()
if self.genkey_function_name not in existing_procedures:
frappe.db.sql(self.genkey_function_sql)
if self.init_procedure_name not in existing_procedures:
frappe.db.sql(self.init_procedure_sql)
if self.allocate_procedure_name not in existing_procedures:
frappe.db.sql(self.allocate_procedure_sql)
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
frappe.db.sql(self._voucher_balance_definition)
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
frappe.db.sql(self._row_def_table_definition)

View File

@@ -779,6 +779,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
"customer_group": "Individual",
}
)
.insert()
@@ -1002,6 +1003,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
"customer_group": "Individual",
}
)
.insert()
@@ -1202,3 +1204,52 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _
from frappe.utils import flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
@@ -15,6 +16,8 @@ def execute(filters=None):
if not filters:
filters = {}
validate_filters(filters)
columns = get_columns(filters)
if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter")
@@ -35,6 +38,21 @@ def execute(filters=None):
return columns, data, None, chart
def validate_filters(filters):
validate_budget_dimensions(filters)
def validate_budget_dimensions(filters):
dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]]
if filters.get("budget_against") and filters.get("budget_against") not in dimensions:
frappe.throw(
title=_("Invalid Accounting Dimension"),
msg=_("{0} is not a valid Accounting Dimension.").format(
frappe.bold(filters.get("budget_against"))
),
)
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
for account, monthwise_data in dimension_items.items():
row = [dimension, account]

View File

@@ -48,6 +48,9 @@ class Deferred_Item:
Generate report data for output
"""
ret_data = frappe._dict({"name": self.item_name})
ret_data.service_start_date = self.service_start_date
ret_data.service_end_date = self.service_end_date
ret_data.amount = self.base_net_amount
for period in self.period_total:
ret_data[period.key] = period.total
ret_data.indent = 1
@@ -205,6 +208,9 @@ class Deferred_Invoice:
for item in self.uniq_items:
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
# roll-up amount from all deferred items
self.amount_total = sum(item.base_net_amount for item in self.items)
def calculate_invoice_revenue_expense_for_period(self):
"""
calculate deferred revenue/expense for all items in invoice
@@ -232,7 +238,7 @@ class Deferred_Invoice:
generate report data for invoice, includes invoice total
"""
ret_data = []
inv_total = frappe._dict({"name": self.name})
inv_total = frappe._dict({"name": self.name, "amount": self.amount_total})
for x in self.period_total:
inv_total[x.key] = x.total
inv_total.indent = 0
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
def get_columns(self):
columns = []
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
columns.append(
{
"label": _("Service Start Date"),
"fieldname": "service_start_date",
"fieldtype": "Date",
"read_only": 1,
}
)
columns.append(
{
"label": _("Service End Date"),
"fieldname": "service_end_date",
"fieldtype": "Date",
"read_only": 1,
}
)
columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1})
for period in self.period_list:
columns.append(
{
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
elif self.filters.type == "Expense":
total_row = frappe._dict({"name": "Total Deferred Expense"})
total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices)
for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total
ret.append(total_row)

View File

@@ -176,10 +176,16 @@ frappe.query_reports["General Ledger"] = {
fieldtype: "Check",
default: 1,
},
{
fieldname: "disable_opening_balance_calculation",
label: __("Disable Opening Balance Calculation"),
fieldtype: "Check",
},
{
fieldname: "show_opening_entries",
label: __("Show Opening Entries"),
fieldtype: "Check",
depends_on: "eval: !doc.disable_opening_balance_calculation",
},
{
fieldname: "include_default_book_entries",

View File

@@ -279,7 +279,15 @@ def get_conditions(filters):
if filters.get("party"):
conditions.append("party in %(party)s")
if not (
if filters.get("disable_opening_balance_calculation"):
if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date >=%(from_date)s")
# opening balance calculation is done only if filtered on account/party
# so from_date filter is not applied
elif not (
filters.get("account")
or filters.get("party")
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
@@ -528,7 +536,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
group_by_value = gle.get(group_by)
gle.voucher_type = gle.voucher_type
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if gle.posting_date < from_date or (
cstr(gle.is_opening) == "Yes"
and not show_opening_entries
and not filters.disable_opening_balance_calculation
):
if not group_by_voucher_consolidated:
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)

View File

@@ -786,19 +786,11 @@ class GrossProfitGenerator:
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, row.item_row, item_code
)
elif self.delivery_notes.get((row.parent, row.item_code), None):
# check if Invoice has delivery notes
dn = self.delivery_notes.get((row.parent, row.item_code))
parenttype, parent, item_row, dn_warehouse = (
"Delivery Note",
dn["delivery_note"],
dn["item_row"],
dn["warehouse"],
)
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif row.item_row and self.delivery_notes.get(row.item_row):
dn = self.delivery_notes[row.item_row]
if flt(dn.total_qty):
return flt(row.qty) * flt(dn.total_incoming_value) / flt(dn.total_qty)
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:
@@ -1049,25 +1041,29 @@ class GrossProfitGenerator:
def get_delivery_notes(self):
self.delivery_notes = frappe._dict({})
if self.si_list:
from frappe.query_builder.functions import Sum
invoices = [x.parent for x in self.si_list]
dni = qb.DocType("Delivery Note Item")
delivery_notes = (
qb.from_(dni)
.select(
dni.against_sales_invoice.as_("sales_invoice"),
dni.item_code,
dni.warehouse,
dni.parent.as_("delivery_note"),
dni.name.as_("item_row"),
dni.si_detail,
Sum(dni.stock_qty * dni.incoming_rate).as_("total_incoming_value"),
Sum(dni.stock_qty).as_("total_qty"),
)
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
.groupby(dni.against_sales_invoice, dni.item_code)
.orderby(dni.creation, order=Order.desc)
.where(
(dni.docstatus == 1)
& (dni.against_sales_invoice.isin(invoices))
& (dni.si_detail.isnotnull())
& (dni.si_detail != "")
)
.groupby(dni.si_detail)
.run(as_dict=True)
)
for entry in delivery_notes:
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
self.delivery_notes[entry.si_detail] = entry
def group_items_by_invoice(self):
"""

View File

@@ -82,6 +82,7 @@ class TestGrossProfit(FrappeTestCase):
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
self.customer = customer.name

View File

@@ -31,6 +31,7 @@ def _execute(filters=None, additional_table_columns=None):
item_list = get_items(filters, additional_table_columns)
aii_account_map = get_aii_accounts()
default_taxes = {}
if item_list:
itemised_tax, tax_columns = get_tax_accounts(
item_list,
@@ -39,6 +40,9 @@ def _execute(filters=None, additional_table_columns=None):
doctype="Purchase Invoice",
tax_doctype="Purchase Taxes and Charges",
)
for tax in tax_columns:
default_taxes[f"{tax}_rate"] = 0
default_taxes[f"{tax}_amount"] = 0
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
@@ -85,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
}
total_tax = 0
row.update(default_taxes.copy())
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(

View File

@@ -29,8 +29,12 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_table_columns, additional_conditions)
default_taxes = {}
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
for tax in tax_columns:
default_taxes[f"{tax}_rate"] = 0
default_taxes[f"{tax}_amount"] = 0
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)
@@ -88,6 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
total_tax = 0
total_other_charges = 0
row.update(default_taxes.copy())
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(

View File

@@ -17,9 +17,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, do_not_submit=False):
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
item=item or self.item,
item_name=item or self.item,
description=item or self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
@@ -30,6 +32,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
price_list_rate=100,
do_not_save=1,
)
for tax in taxes or []:
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax["account_head"],
"cost_center": self.cost_center,
"description": tax["description"],
"rate": tax["rate"],
},
)
si = si.save()
if not do_not_submit:
si = si.submit()
@@ -63,3 +78,50 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_grouped_report_handles_different_tax_descriptions(self):
self.create_item(item_name="_Test Item Tax Description A")
first_item = self.item
self.create_item(item_name="_Test Item Tax Description B")
second_item = self.item
first_tax_description = "Tax Description A"
second_tax_description = "Tax Description B"
first_tax_amount_field = f"{frappe.scrub(first_tax_description)}_amount"
second_tax_amount_field = f"{frappe.scrub(second_tax_description)}_amount"
self.create_sales_invoice(
item=first_item,
taxes=[
{
"account_head": "_Test Account VAT - _TC",
"description": first_tax_description,
"rate": 5,
}
],
)
self.create_sales_invoice(
item=second_item,
taxes=[
{
"account_head": "_Test Account Service Tax - _TC",
"description": second_tax_description,
"rate": 2,
}
],
)
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"group_by": "Customer",
}
)
_, data, _, _, _, _ = execute(filters)
grand_total_row = next(row for row in data if row.get("bold") and row.get("item_code") == "Total")
self.assertEqual(grand_total_row[first_tax_amount_field], 5.0)
self.assertEqual(grand_total_row[second_tax_amount_field], 2.0)

View File

@@ -501,7 +501,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestPurchaseRegister(FrappeTestCase):
@@ -26,6 +27,52 @@ class TestPurchaseRegister(FrappeTestCase):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")

View File

@@ -5,6 +5,7 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
@@ -75,6 +76,43 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
report_output = {k: v for k, v in res[0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
res = [x for x in report[1] if x.get("voucher_no") == si.name]
self.assertEqual(len(res), 1)
result = frappe._dict(res[0])
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -119,8 +119,8 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
row.update(
{
"section_code": tax_withholding_category or "",
"entity_type": party_map.get(party, {}).get(party_type),
"tax_withholding_category": tax_withholding_category or "",
"party_entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
"grand_total": grand_total,
@@ -141,7 +141,7 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
else:
entries[key] = row
out = list(entries.values())
out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
out.sort(key=lambda x: (x["tax_withholding_category"], x["transaction_date"], x["ref_no"]))
return out
@@ -205,9 +205,9 @@ def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{
"label": _("Section Code"),
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 90,
},
@@ -236,7 +236,12 @@ def get_columns(filters):
columns.extend(
[
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
{
"label": _(f"{filters.get('party_type', 'Party')} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 100,
},
]
)
if filters.party_type == "Supplier":

View File

@@ -118,7 +118,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
voucher_expected_values = expected_values[i]
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.tax_withholding_category,
voucher.rate,
voucher.base_tax_withholding_net_total,
voucher.base_total,

View File

@@ -48,28 +48,25 @@ def group_by_party_and_category(data, filters):
party_category_wise_map = {}
for row in data:
key = (row.get("party_type"), row.get("party"), row.get("tax_withholding_category"))
party_category_wise_map.setdefault(
(row.get("party"), row.get("section_code")),
key,
{
"pan": row.get("pan"),
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_type": row.get("party_type"),
"party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
"tax_withholding_category": row.get("tax_withholding_category"),
"party_entity_type": row.get("party_entity_type"),
"rate": row.get("rate"),
"total_amount": 0.0,
"tax_amount": 0.0,
},
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
"total_amount", 0.0
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
"tax_amount", 0.0
)
party_category_wise_map.get(key)["total_amount"] += row.get("total_amount", 0.0)
party_category_wise_map.get(key)["tax_amount"] += row.get("tax_amount", 0.0)
final_result = get_final_result(party_category_wise_map)
@@ -110,13 +107,18 @@ def get_columns(filters):
columns.extend(
[
{
"label": _("Section Code"),
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
{
"label": _(f"{filters.get('party_type', 'Party')} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",

View File

@@ -12,6 +12,7 @@ class AccountsTestMixin:
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
customer.customer_group = "Individual"
if currency:
customer.default_currency = currency
@@ -36,6 +37,7 @@ class AccountsTestMixin:
"account": default_account,
},
)
customer.customer_group = "Individual"
customer.save()
self.customer = customer_name

View File

@@ -7,12 +7,8 @@ from erpnext.accounts.party import get_default_price_list
class PartyTestCase(FrappeTestCase):
def test_get_default_price_list_should_return_none_for_invalid_group(self):
customer = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "test customer",
}
{"doctype": "Customer", "customer_name": "test customer", "customer_group": "Individual"}
).insert(ignore_permissions=True, ignore_mandatory=True)
customer.customer_group = None
customer.save()
price_list = get_default_price_list(customer)
assert price_list is None

View File

@@ -500,7 +500,7 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount"):
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)
@@ -2320,6 +2320,7 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2346,6 +2347,7 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2363,6 +2365,7 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
});
},
onload: (frm) => {
refresh: (frm) => {
frm.trigger("set_required_fields");
},

View File

@@ -440,7 +440,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -448,7 +452,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100) {
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -705,12 +709,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
field_copy.push("project");
}
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
@@ -785,12 +797,6 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -826,18 +826,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.set_payment_schedule()
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
def update_item(obj, target, source_parent):
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
billed_qty = flt(get_billed_qty(obj.name))
target.qty = flt(obj.qty) - billed_qty
@@ -877,7 +877,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
"condition": lambda doc: (
doc.base_amount == 0
or abs(doc.billed_amt) < abs(doc.amount)
or doc.qty > flt(get_billed_qty(doc.name))
)
and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
@@ -912,6 +916,8 @@ def get_list_context(context=None):
@frappe.whitelist()
def update_status(status, name):
frappe.has_permission("Purchase Order", "submit", name, throw=True)
po = frappe.get_doc("Purchase Order", name)
po.update_status(status)
po.update_delivered_qty_in_sales_order()

View File

@@ -289,6 +289,30 @@ class TestPurchaseOrder(FrappeTestCase):
# ordered qty should decrease (back to initial) on row deletion
self.assertEqual(get_ordered_qty(), existing_ordered_qty)
def test_discount_amount_partial_purchase_receipt(self):
po = create_purchase_order(qty=4, rate=100, do_not_save=1)
po.apply_discount_on = "Grand Total"
po.discount_amount = 120
po.save()
po.submit()
self.assertEqual(po.grand_total, 280)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 3
pr1.save()
pr1.submit()
self.assertEqual(pr1.discount_amount, 120)
self.assertEqual(pr1.grand_total, 180)
pr2 = make_purchase_receipt(po.name)
pr2.save()
pr2.submit()
self.assertEqual(pr2.discount_amount, 0)
self.assertEqual(pr2.grand_total, 100)
def test_update_child_perm(self):
po = create_purchase_order(item_code="_Test Item", qty=4)
@@ -1346,6 +1370,35 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed")
@change_settings("Buying Settings", {"maintain_same_rate": 0})
def test_purchase_order_over_billing_missing_item(self):
item1 = make_item(
"_Test Item for Overbilling",
).name
item2 = make_item(
"_Test Item for Overbilling 2",
).name
po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1)
po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"})
po.taxes = []
po.insert()
po.submit()
pi1 = make_pi_from_po(po.name)
pi1.items[0].qty = 8
pi1.items[0].rate = 1250
pi1.remove(pi1.items[1])
pi1.insert()
pi1.submit()
self.assertEqual(pi1.grand_total, 10000.0)
self.assertTrue(len(pi1.items) == 1)
pi2 = make_pi_from_po(po.name)
self.assertEqual(len(pi2.items), 2)
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -283,7 +283,7 @@ class RequestforQuotation(BuyingController):
}
)
user.save(ignore_permissions=True)
update_password_link = user.reset_password()
update_password_link = user._reset_password()
return user, update_password_link
@@ -474,6 +474,11 @@ def create_supplier_quotation(doc):
if isinstance(doc, str):
doc = json.loads(doc)
if frappe.session.user not in frappe.get_all(
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
try:
sq_doc = frappe.get_doc(
{

View File

@@ -263,6 +263,13 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
for data in supplier_data:
rfq.append("suppliers", data)
frappe.new_doc(
"Portal User",
user="Administrator",
parent=data.get("supplier"),
parentfield="portal_users",
parenttype="Supplier",
).insert()
rfq.append(
"items",

View File

@@ -175,6 +175,15 @@ def create_supplier(**args):
if not args.without_supplier_group:
doc.supplier_group = args.supplier_group or "Services"
if args.get("party_account"):
doc.append(
"accounts",
{
"company": frappe.db.get_value("Account", args.get("party_account"), "company"),
"account": args.get("party_account"),
},
)
doc.insert()
return doc

View File

@@ -115,9 +115,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
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

@@ -41,6 +41,7 @@ def get_columns(filters):
"fieldname": "transferred_qty",
"width": 200,
},
{"label": _("Returned Quantity"), "fieldtype": "Float", "fieldname": "returned_qty", "width": 150},
{"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150},
]
@@ -50,7 +51,7 @@ def get_data(filters):
data = []
for row in order_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0
transferred_qty = (row.get("transferred_qty") or 0) - (row.get("returned_qty") or 0)
if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
row.p_qty = pending_qty if pending_qty > 0 else 0
@@ -86,6 +87,7 @@ def get_order_items_to_supply(filters):
f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
f"`tab{supplied_items_table}`.supplied_qty as transferred_qty",
f"`tab{supplied_items_table}`.returned_qty as returned_qty",
],
filters=record_filters,
)

View File

@@ -68,6 +68,7 @@ from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
_get_item_tax_template,
_get_item_tax_template_from_item_group,
get_conversion_factor,
get_item_details,
get_item_tax_map,
@@ -325,6 +326,7 @@ class AccountsController(TransactionBase):
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Purchase Invoice",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
@@ -1751,6 +1753,7 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1835,6 +1838,7 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -2297,6 +2301,16 @@ class AccountsController(TransactionBase):
return stock_items
def get_asset_items(self):
asset_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
asset_items = frappe.db.get_values(
"Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True
)
return asset_items
def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
return (
@@ -3634,6 +3648,10 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
}
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
if not child_item.get("item_tax_template"):
child_item.item_tax_template = _get_item_tax_template_from_item_group(args, item.item_group)
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(
parent_doc.get("company"), child_item.item_tax_template, as_json=True

View File

@@ -327,7 +327,7 @@ class BuyingController(SubcontractingController):
last_item_idx = d.idx
total_valuation_amount = sum(
flt(d.base_tax_amount_after_discount_amount)
flt(d.base_tax_amount_after_discount_amount) * (-1 if d.get("add_deduct_tax") == "Deduct" else 1)
for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]
)

View File

@@ -356,38 +356,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
doctype = "Delivery Note"
def get_delivery_notes_to_be_billed(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
):
DeliveryNote = frappe.qb.DocType("Delivery Note")
fields = get_fields(doctype, ["name", "customer", "posting_date"])
return frappe.db.sql(
"""
select {fields}
from `tabDelivery Note`
where `tabDelivery Note`.`{key}` like {txt} and
`tabDelivery Note`.docstatus = 1
and status not in ('Stopped', 'Closed') {fcond}
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
original_dn = (
frappe.qb.from_(DeliveryNote)
.select(DeliveryNote.name)
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
& (
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
| (
(DeliveryNote.is_return == 1)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.return_against.isin(original_dn))
)
)
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
""".format(
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
key=searchfield,
fcond=get_filters_cond(doctype, filters, []),
mcond=get_match_cond(doctype),
start=start,
page_len=page_len,
txt="%(txt)s",
),
{"txt": ("%%%s%%" % txt)},
as_dict=as_dict,
)
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)
@frappe.whitelist()
@@ -992,3 +997,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
limit_page_length=page_len,
as_list=1,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
table = frappe.qb.DocType(doctype)
child_table = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(table)
.inner_join(child_table)
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
.select(table.name)
.where(
(child_table.link_name == filters.get("warehouse"))
& (table.disabled == 0)
& (child_table.link_doctype == "Warehouse")
& (table.name.like(f"%{txt}%"))
)
.offset(start)
.limit(page_len)
)
return query.run(as_list=1)

View File

@@ -616,11 +616,11 @@ class SellingController(StockController):
if allow_at_arms_length_price:
continue
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
if flt(d.rate, d.precision("incoming_rate")) != flt(
rate, d.precision("incoming_rate")
):
d.rate = rate
frappe.msgprint(
_(

View File

@@ -234,8 +234,8 @@ class StatusUpdater(Document):
self.global_amount_allowance = None
for args in self.status_updater:
if "target_ref_field" not in args:
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
if "target_ref_field" not in args or args.get("validate_qty") is False:
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
continue
# get unique transactions to update

View File

@@ -989,6 +989,12 @@ class SubcontractingController(StockController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "BOM"
):
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if not self.__transferred_items or not self.__transferred_items.get(key):

View File

@@ -183,8 +183,10 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
do_not_round_fields = ["valuation_rate", "incoming_rate"]
for item in self.doc.items:
self.doc.round_floats_in(item)
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
if item.discount_percentage == 100:
item.rate = 0.0
@@ -674,18 +676,17 @@ class calculate_taxes_and_totals:
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = 0
self.doc.base_rounded_total = 0
self.doc.rounding_adjustment = 0
return
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
else:
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
# rounding adjustment should always be the difference vetween grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
# rounding adjustment should always be the difference between grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
@@ -724,7 +725,8 @@ class calculate_taxes_and_totals:
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
# only during save (i.e. when `_action` is set)
if self.doc.get("_action") and (
(grand_total >= 0 and discount_amount > grand_total)
or (grand_total < 0 and discount_amount < grand_total) # returns
):

View File

@@ -29,6 +29,7 @@ def make_customer(customer_name, currency=None):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.customer_type = "Individual"
customer.customer_group = "Individual"
if currency:
customer.default_currency = currency

View File

@@ -66,7 +66,7 @@ class TestTaxes(unittest.TestCase):
{
"doctype": "Customer",
"customer_name": uuid4(),
"customer_group": "All Customer Groups",
"customer_group": "Individual",
}
).insert()
self.supplier = frappe.get_doc(

View File

@@ -0,0 +1,37 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestTaxesAndTotals(FrappeTestCase):
def test_disabling_rounded_total_resets_base_fields(self):
"""Disabling rounded total should also clear base rounded values."""
so = make_sales_order(do_not_save=True)
so.items[0].qty = 1
so.items[0].rate = 1000.25
so.items[0].price_list_rate = 1000.25
so.items[0].discount_percentage = 0
so.items[0].discount_amount = 0
so.set("taxes", [])
so.disable_rounded_total = 0
calculate_taxes_and_totals(so)
self.assertEqual(so.grand_total, 1000.25)
self.assertEqual(so.rounded_total, 1000.0)
self.assertEqual(so.rounding_adjustment, -0.25)
self.assertEqual(so.base_grand_total, 1000.25)
self.assertEqual(so.base_rounded_total, 1000.0)
self.assertEqual(so.base_rounding_adjustment, -0.25)
# User toggles disable_rounded_total after values are already set.
so.disable_rounded_total = 1
calculate_taxes_and_totals(so)
self.assertEqual(so.rounded_total, 0)
self.assertEqual(so.rounding_adjustment, 0)
self.assertEqual(so.base_rounded_total, 0)
self.assertEqual(so.base_rounding_adjustment, 0)

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import DateTimeLikeObject, getdate, today
from erpnext.accounts.utils import get_fiscal_year
def get_columns(filters, trans):
@@ -45,6 +47,10 @@ def get_columns(filters, trans):
def validate_filters(filters):
if not filters.get("fiscal_year"):
filters["fiscal_year"] = get_fiscal_year(today())[0]
if not filters.get("company"):
filters["company"] = frappe.defaults.get_user_default("Company")
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
if not filters.get(f.lower().replace(" ", "_")):
frappe.throw(_("{0} is mandatory").format(_(f)))
@@ -55,6 +61,14 @@ def validate_filters(filters):
if filters.get("based_on") == filters.get("group_by"):
frappe.throw(_("'Based On' and 'Group By' can not be same"))
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
frappe.throw(
msg=_("{0} can be either {1} or {2}.").format(
frappe.bold("Period based On"), frappe.bold("Posting Date"), frappe.bold("Billing Date")
),
title=_("Invalid Filter"),
)
def get_data(filters, conditions):
data = []

View File

@@ -234,10 +234,13 @@ def _get_agents_sorted_by_asc_workload(date):
return agent_list
appointment_counter = Counter(agent_list)
for appointment in appointments:
assigned_to = frappe.parse_json(appointment._assign)
if not assigned_to:
assign_data = appointment._assign
if isinstance(assign_data, str):
assign_data = assign_data.strip()
if not assign_data:
continue
if (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
assigned_to = frappe.parse_json(assign_data)
if assigned_to and (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
appointment_counter[assigned_to[0]] += 1
sorted_agent_list = appointment_counter.most_common()
sorted_agent_list.reverse()

View File

@@ -56,7 +56,7 @@
}
],
"links": [],
"modified": "2020-12-07 10:44:22.587047",
"modified": "2026-03-25 19:27:19.162421",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract Template",
@@ -75,43 +75,34 @@
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"write": 1
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
# called from hooks on doc_event Email Unsubscribe
def unsubscribe_recipient(unsubscribe, method):
if unsubscribe.reference_doctype == "Email Campaign":
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
if unsubscribe.reference_doctype != "Email Campaign":
return
email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name)
if email_campaign.email_campaign_for == "Email Group":
if unsubscribe.email:
frappe.db.set_value(
"Email Group Member",
{"email_group": email_campaign.recipient, "email": unsubscribe.email},
"unsubscribed",
1,
)
else:
# For Lead or Contact
frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed")
# called through hooks to update email campaign status daily

View File

@@ -35,7 +35,9 @@ class TestOpportunity(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Lead", opp_doc.party_name, "email_id"), opp_doc.contact_email)
# create new customer and create new contact against 'new.opportunity@example.com'
customer = make_customer(opp_doc.party_name).insert(ignore_permissions=True)
customer = make_customer(opp_doc.party_name)
customer.customer_group = "Individual"
customer.insert(ignore_permissions=True)
contact = frappe.get_doc(
{
"doctype": "Contact",

View File

@@ -117,7 +117,7 @@ def get_join(filters):
join = """JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
""".format(filters.get("lost_reason"))
`tabOpportunity Lost Reason Detail`.lost_reason=%(lost_reason)s
"""
return join

View File

@@ -196,6 +196,7 @@ def create_customer():
if not doc:
doc = frappe.new_doc("Customer")
doc.customer_name = "_Test NC"
doc.customer_group = "Individual"
doc.insert()

View File

@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
import frappe
from frappe.model.document import Document
from frappe.utils import escape_html
if TYPE_CHECKING:
from lxml.etree import Element
@@ -63,14 +64,16 @@ class CodeList(Document):
def from_genericode(self, root: "Element"):
"""Extract Code List details from genericode XML"""
self.title = root.find(".//Identification/ShortName").text
self.title = escape_html(root.find(".//Identification/ShortName").text)
self.version = root.find(".//Identification/Version").text
self.canonical_uri = root.find(".//CanonicalUri").text
# optionals
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None))
self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None))
if not self.publisher:
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
self.publisher = escape_html(
getattr(root.find(".//Identification/Agency/LongName"), "text", None)
)
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)

View File

@@ -10,6 +10,7 @@ erpnext.edi.import_genericode = function (listview_or_form) {
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
doctype: doctype,
docname: docname,
allow_web_link: false,
allow_toggle_private: false,
allow_take_photo: false,
on_success: function (_file_doc, r) {

View File

@@ -1,42 +1,118 @@
import json
from urllib.parse import urlsplit
import frappe
import requests
from frappe import _
from frappe.utils import escape_html
from frappe.utils.file_manager import save_file
from lxml import etree
URL_PREFIXES = ("http://", "https://")
GENERICODE_FETCH_TIMEOUT = 15
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
class RemoteGenericodeUrlNotAllowedError(Exception):
pass
class CodeListSelectionMismatchError(Exception):
pass
@frappe.whitelist()
def import_genericode():
doctype = "Code List"
docname = frappe.form_dict.docname
content = frappe.local.uploaded_file
# recover the content, if it's a link
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
try:
# If it's a URL, fetch the content and make it a local file (for durable audit)
response = requests.get(frappe.local.uploaded_file_url)
response.raise_for_status()
frappe.local.uploaded_file = content = response.content
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
frappe.local.uploaded_file_url = None
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
if file_url := frappe.local.uploaded_file_url:
file_path = frappe.utils.file_manager.get_file_path(file_url)
with open(file_path.encode(), mode="rb") as f:
content = f.read()
# Parse the xml content
parser = etree.XMLParser(remove_blank_text=True)
try:
root = etree.fromstring(content, parser=parser)
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
content, file_name = get_uploaded_genericode_file()
return import_genericode_content(
doctype="Code List",
docname=frappe.form_dict.docname,
content=content,
file_name=file_name,
)
except RemoteGenericodeUrlNotAllowedError:
frappe.throw(
_("Importing Code Lists from remote URLs is not allowed."),
title=_("Invalid Upload"),
)
except CodeListSelectionMismatchError:
frappe.throw(_("The uploaded file does not match the selected Code List."))
except etree.XMLSyntaxError:
frappe.throw(
_("The uploaded file could not be parsed as a genericode XML document."),
title=_("Parsing Error"),
)
def import_genericode_from_url(
url: str,
doctype: str = "Code List",
docname: str | None = None,
):
"""Import a Code List from a trusted backend URL."""
content = fetch_genericode_from_url(url)
file_name = urlsplit(url).path.rsplit("/", 1)[-1] or "genericode.xml"
return import_genericode_content(
doctype=doctype,
docname=docname,
content=content,
file_name=file_name,
)
def get_uploaded_genericode_file() -> tuple[bytes, str | None]:
uploaded_data = frappe.local.uploaded_file
file_name = frappe.local.uploaded_filename
if uploaded_data and file_name:
return uploaded_data, file_name
file_url = frappe.local.uploaded_file_url
if not file_url:
raise frappe.ValidationError(_("No file uploaded or URL provided."))
if not is_local_file_url(file_url):
raise RemoteGenericodeUrlNotAllowedError
file_doc = frappe.get_doc("File", {"file_url": file_url})
file_doc.check_permission("read")
return read_file_bytes(file_doc), file_name
def read_file_bytes(file_doc) -> bytes:
"""Return the raw bytes of a File document.
v15's `File.get_content` eagerly decodes to utf-8 and returns `str` for text
files, but `lxml.etree.fromstring` needs bytes when the XML declares an encoding.
"""
content = file_doc.get_content()
if isinstance(content, str):
content = content.encode("utf-8")
return content
def is_local_file_url(file_url: str | None) -> bool:
if not file_url:
return False
parsed = urlsplit(file_url.strip())
return not parsed.scheme and not parsed.netloc and parsed.path.startswith(LOCAL_FILE_PREFIXES)
def fetch_genericode_from_url(url: str) -> bytes:
response = requests.get(url, timeout=GENERICODE_FETCH_TIMEOUT)
response.raise_for_status()
return response.content
def import_genericode_content(
doctype: str,
docname: str | None,
content: bytes,
file_name: str | None,
):
root = parse_genericode_content(content)
# Extract the name (CanonicalVersionUri) from the parsed XML
name = root.find(".//CanonicalVersionUri").text
@@ -45,7 +121,7 @@ def import_genericode():
if frappe.db.exists(doctype, docname):
code_list = frappe.get_doc(doctype, docname)
if code_list.name != name:
frappe.throw(_("The uploaded file does not match the selected Code List."))
raise CodeListSelectionMismatchError
else:
# Create a new Code List document with the extracted name
code_list = frappe.new_doc(doctype)
@@ -54,19 +130,13 @@ def import_genericode():
code_list.from_genericode(root)
code_list.save()
# Attach the file and provide a recoverable identifier
file_doc = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,
"content": content,
}
).save()
file_doc = save_file(
fname=file_name,
content=content,
dt=doctype,
dn=code_list.name,
is_private=1,
)
# Get available columns and example values
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
@@ -81,6 +151,16 @@ def import_genericode():
}
def parse_genericode_content(content: bytes):
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
return etree.fromstring(content, parser=parser)
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,
@@ -104,7 +184,7 @@ def get_genericode_columns_and_examples(root):
# Get column names
for column in root.findall(".//Column"):
column_id = column.get("Id")
column_id = escape_html(column.get("Id"))
columns.append(column_id)
example_values[column_id] = []
filterable_columns[column_id] = set()
@@ -112,7 +192,7 @@ def get_genericode_columns_and_examples(root):
# Get all values and count unique occurrences
for row in root.findall(".//SimpleCodeList/Row"):
for value in row.findall("Value"):
column_id = value.get("ColumnRef")
column_id = escape_html(value.get("ColumnRef"))
if column_id not in columns:
# Handle undeclared column
columns.append(column_id)
@@ -123,7 +203,7 @@ def get_genericode_columns_and_examples(root):
if simple_value is None:
continue
filterable_columns[column_id].add(simple_value.text)
filterable_columns[column_id].add(escape_html(simple_value.text))
# Get example values (up to 3) and filter columns with cardinality <= 5
for row in root.findall(".//SimpleCodeList/Row")[:3]:
@@ -133,7 +213,7 @@ def get_genericode_columns_and_examples(root):
if simple_value is None:
continue
example_values[column_id].append(simple_value.text)
example_values[column_id].append(escape_html(simple_value.text))
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}

View File

@@ -0,0 +1,200 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import Mock, patch
import frappe
import requests
from frappe.tests.utils import FrappeTestCase
from erpnext.edi.doctype.code_list import code_list_import
SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
<CodeList>
<Identification>
<ShortName>Test Code List</ShortName>
<Version>1.0</Version>
<CanonicalUri>test-code-list</CanonicalUri>
<LongName>Code list for tests</LongName>
<Agency>
<ShortName>Test Agency</ShortName>
<Identifier>TEST</Identifier>
</Agency>
<LocationUri>https://example.com/codelists/test.xml</LocationUri>
</Identification>
<CanonicalVersionUri>test-code-list-v1</CanonicalVersionUri>
<ColumnSet>
<Column Id="code" />
<Column Id="name" />
<Column Id="category" />
</ColumnSet>
<SimpleCodeList>
<Row>
<Value ColumnRef="code"><SimpleValue>A</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Alpha</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>B</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Beta</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 2</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>C</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Gamma</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
</SimpleCodeList>
</CodeList>
"""
class TestCodeListImport(FrappeTestCase):
def test_import_genericode_rejects_remote_file_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="https://example.com/codelists/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_rejects_file_scheme_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="file:///tmp/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_from_trusted_url(self):
response = Mock()
response.content = SAMPLE_GENERICODE
response.raise_for_status.return_value = None
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
return_value=response,
) as mock_get:
import_result = code_list_import.import_genericode_from_url(
"https://example.com/codelists/trusted.xml"
)
self.assert_import_response(import_result)
mock_get.assert_called_once_with(
"https://example.com/codelists/trusted.xml",
timeout=code_list_import.GENERICODE_FETCH_TIMEOUT,
)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
self.assertFalse(file_doc.file_url.startswith("https://"))
def test_import_genericode_from_trusted_url_propagates_fetch_errors(self):
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
side_effect=requests.Timeout,
):
with self.assertRaises(requests.Timeout):
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
def test_import_genericode_from_uploaded_file_returns_metadata(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
def test_process_genericode_import_reads_file_doc_content(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
count = code_list_import.process_genericode_import(
code_list_name=import_result["code_list"],
file_name=import_result["file"],
code_column="code",
title_column="name",
)
self.assertEqual(count, 3)
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
self.assertEqual(
frappe.db.get_value(
"Common Code",
{"code_list": import_result["code_list"], "common_code": "A"},
"title",
),
"Alpha",
)
def test_import_genericode_from_local_file_url(self):
source_file = frappe.get_doc(
{
"doctype": "File",
"file_name": "library_genericode.xml",
"content": SAMPLE_GENERICODE,
"is_private": 1,
}
).insert()
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
def set_upload_context(
self,
content: bytes | None = None,
file_name: str = "genericode.xml",
file_url: str | None = None,
docname: str | None = None,
):
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
frappe.local.uploaded_file = content
frappe.local.uploaded_file_url = file_url
frappe.local.uploaded_filename = file_name
def restore():
for attr, value in originals.items():
setattr(frappe.local, attr, value)
self.addCleanup(restore)
def assert_import_response(self, import_result):
self.assertEqual(
set(import_result),
{
"code_list",
"code_list_title",
"file",
"columns",
"example_values",
"filterable_columns",
},
)
self.assertEqual(import_result["code_list"], "test-code-list-v1")
self.assertEqual(import_result["code_list_title"], "Test Code List")
self.assertEqual(import_result["columns"], ["code", "name", "category"])
self.assertEqual(import_result["example_values"]["code"], ["A", "B", "C"])
self.assertEqual(import_result["example_values"]["name"], ["Alpha", "Beta", "Gamma"])
self.assertEqual(import_result["example_values"]["category"], ["Group 1", "Group 2", "Group 1"])
self.assertCountEqual(import_result["filterable_columns"]["category"], ["Group 1", "Group 2"])
self.assertTrue(frappe.db.exists("Code List", import_result["code_list"]))
self.assertTrue(frappe.db.exists("File", import_result["file"]))

View File

@@ -9,6 +9,8 @@ from frappe.model.document import Document
from frappe.utils.data import get_link_to_form
from lxml import etree
from erpnext.edi.doctype.code_list.code_list_import import parse_genericode_content, read_file_bytes
class CommonCode(Document):
# begin: auto-generated types
@@ -86,15 +88,15 @@ def simple_hash(input_string, length=6):
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
"""Import genericode file and create Common Code entries"""
file_path = frappe.utils.file_manager.get_file_path(file_name)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser=parser)
root = tree.getroot()
file_doc = frappe.get_doc("File", file_name)
file_doc.check_permission("read")
root = parse_genericode_content(read_file_bytes(file_doc))
# Construct the XPath expression
xpath_expr = ".//SimpleCodeList/Row"
filter_conditions = [
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'"
for column_ref, value in (filters or {}).items()
]
if filter_conditions:
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
@@ -102,7 +104,7 @@ def import_genericode(code_list: str, file_name: str, column_map: dict, filters:
elements = root.xpath(xpath_expr)
total_elements = len(elements)
for i, xml_element in enumerate(elements, start=1):
common_code: "CommonCode" = frappe.new_doc("Common Code")
common_code: CommonCode = frappe.new_doc("Common Code")
common_code.code_list = code_list
common_code.from_genericode(column_map, xml_element)
common_code.save()

View File

@@ -120,7 +120,7 @@ class BlanketOrder(Document):
def validate_item_qty(self):
for d in self.items:
if d.qty < 0:
if flt(d.qty) < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))

View File

@@ -192,6 +192,7 @@ frappe.ui.form.on("BOM", {
bom_no: frm.doc.name,
item: item,
qty: data.qty || 0.0,
company: frm.doc.company,
project: frm.doc.project,
variant_items: variant_items,
use_multi_level_bom: use_multi_level_bom,
@@ -263,6 +264,7 @@ frappe.ui.form.on("BOM", {
reqd: 1,
default: 1,
onchange: () => {
if (!cur_dialog) return;
const { quantity, items: rm } = frm.doc;
const variant_items_map = rm.reduce((acc, item) => {
acc[item.item_code] = item.qty;
@@ -758,6 +760,8 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) {
if (d.sourced_by_supplier) {
d.rate = 0;
refresh_field("rate", d.name, d.parentfield);
} else {
get_bom_material_detail(frm.doc, cdt, cdn, false);
}
});

View File

@@ -767,12 +767,14 @@ class BOM(WebsiteGenerator):
hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate
)
if row.hour_rate and row.time_in_mins:
if row.hour_rate:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
if row.time_in_mins:
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
if update_hour_rate:
row.db_update()
@@ -1019,6 +1021,12 @@ class BOM(WebsiteGenerator):
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
).format(d.idx, d.operation)
)
if not d.time_in_mins or d.time_in_mins <= 0:
frappe.throw(
_("Row {0}: Operation time should be greater than 0 for operation {1}").format(
d.idx, d.operation
)
)
def get_tree_representation(self) -> BOMTree:
"""Get a complete tree representation preserving order of child items."""
@@ -1329,9 +1337,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
bom = frappe.get_doc("BOM", work_order.bom_no)
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
items = {}
items = frappe._dict()
for d in bom.get(table):
items.setdefault(d.item_code, d.amount)
items.setdefault(d.item_code, 0)
items[d.item_code] += flt(d.amount)
non_stock_items = frappe.get_all(
"Item",

View File

@@ -132,6 +132,15 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
@timeout
def test_bom_no_operation_time_validation(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
for op_row in bom.operations:
op_row.time_in_mins = 0
self.assertRaises(frappe.ValidationError, bom.save)
@timeout
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])

View File

@@ -47,6 +47,14 @@ frappe.ui.form.on("Job Card", {
},
};
});
frm.set_query("work_order", function () {
return {
filters: {
status: ["not in", ["Cancelled", "Closed", "Stopped"]],
},
};
});
},
refresh: function (frm) {

View File

@@ -15,6 +15,7 @@
"bom_section",
"update_bom_costs_automatically",
"column_break_lhyt",
"allow_editing_of_items_and_quantities_in_work_order",
"section_break_6",
"default_wip_warehouse",
"default_fg_warehouse",
@@ -243,13 +244,20 @@
"fieldname": "enforce_time_logs",
"fieldtype": "Check",
"label": "Enforce Time Logs"
},
{
"default": "0",
"description": "If enabled, the system will allow users to edit the raw materials and their quantities in the Work Order. The system will not reset the quantities as per the BOM, if the user has changed them.",
"fieldname": "allow_editing_of_items_and_quantities_in_work_order",
"fieldtype": "Check",
"label": "Allow Editing of Items and Quantities in Work Order"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-16 11:23:16.916512",
"modified": "2025-11-07 14:52:56.241459",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -18,6 +18,7 @@ class ManufacturingSettings(Document):
from frappe.types import DF
add_corrective_operation_cost_in_finished_good_valuation: DF.Check
allow_editing_of_items_and_quantities_in_work_order: DF.Check
allow_overtime: DF.Check
allow_production_on_holidays: DF.Check
backflush_raw_materials_based_on: DF.Literal["BOM", "Material Transferred for Manufacture"]

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