Compare commits

..

402 Commits

Author SHA1 Message Date
Frappe PR Bot
1ffd814f92 chore(release): Bumped to Version 15.101.0
# [15.101.0](https://github.com/frappe/erpnext/compare/v15.100.2...v15.101.0) (2026-03-10)

### Bug Fixes

* **accounts:** compute tax net_amount in JS controller ([6ad84d6](6ad84d66cc))
* **accounts:** round and convert net_amount to company currency in JS tax controller ([516ad90](516ad9021b))
* balance qty for inv dimension ([6898d70](6898d70382))
* better validation message for Purchase Invoice with Update Stock ([b7fd9ae](b7fd9aea6a))
* client-side taxes calculation ([#44510](https://github.com/frappe/erpnext/issues/44510)) ([717c5b2](717c5b25eb)), closes [#44328](https://github.com/frappe/erpnext/issues/44328)
* correct logic for repair cost in asset repair ([c71557f](c71557f432))
* disallow all actions on job card if work order is closed ([7b2e483](7b2e4832aa))
* enforce permission check for purchase invoice and update test to use service expense account ([a6dd078](a6dd07802a))
* **gross-profit:** apply precision-based rounding to grouped totals ([b59dc17](b59dc173b8))
* **help:** escape query (backport [#53192](https://github.com/frappe/erpnext/issues/53192)) ([#53194](https://github.com/frappe/erpnext/issues/53194)) ([ba4a99b](ba4a99b22c))
* **manufacturing:** ignore sales order validation for subassembly item ([624d1d4](624d1d4759))
* **manufacturing:** show returned qty in progress bar ([260d87a](260d87a80c))
* removed non existent patch ([fd8fac7](fd8fac7d40))
* **selling:** update delivery date in line items ([dfbb3e9](dfbb3e97a8))
* set default repair cost to 0 if no value is returned ([0b1746a](0b1746a4c8))
* skip asset sale processing for internal transfer invoices ([a7e8f31](a7e8f31f56))
* stock balance report qty ([180e232](180e232eb0))
* **test:** ensure warehouse is consistently referenced in asset repair tests ([ed428ce](ed428ceb1c))
* **test:** include warehouse parameter in asset repair test case ([bcc542b](bcc542b1f9))
* updating costing based on employee change in timesheet ([be59810](be598108b6))
* validation for cancellation ([c142a2b](c142a2be9c))

### Features

* allowing rate modification in update item in quotation (backport [#53147](https://github.com/frappe/erpnext/issues/53147)) ([#53150](https://github.com/frappe/erpnext/issues/53150)) ([072ab8d](072ab8d5f3))
* **manufacturing:** show disassembled qty in progress bar ([c572a01](c572a019b4))
2026-03-10 14:48:03 +00:00
ruthra kumar
c6e7cf13b5 Merge pull request #53293 from frappe/version-15-hotfix
chore: release v15
2026-03-10 20:16:23 +05:30
ruthra kumar
abe433cfa7 Merge pull request #53296 from frappe/mergify/bp/version-15-hotfix/pr-53071
fix(gross-profit): apply precision-based rounding to grouped totals (backport #53071)
2026-03-10 16:55:44 +05:30
Khushi Rawat
49648b5c6e Merge pull request #53258 from ljain112/backport-50804
fix: correct logic for repair cost in asset repair
2026-03-10 16:46:53 +05:30
Navin-S-R
b59dc173b8 fix(gross-profit): apply precision-based rounding to grouped totals
(cherry picked from commit 52dd7665e7)
2026-03-10 11:08:07 +00:00
rohitwaghchaure
7454db2b3e Merge pull request #53288 from frappe/mergify/bp/version-15-hotfix/pr-53283
fix: removed non existent patch (backport #53283)
2026-03-10 14:48:11 +05:30
mergify[bot]
fcfadf9dea Merge pull request #53286 from frappe/mergify/bp/version-15-hotfix/pr-53282
fix: allow user to make QI after submission not working (backport #53282)
2026-03-10 09:12:20 +00:00
rohitwaghchaure
098f6fd0d2 Merge pull request #53284 from frappe/mergify/bp/version-15-hotfix/pr-53281
fix: better validation message for Purchase Invoice with Update Stock (backport #53281)
2026-03-10 14:29:44 +05:30
Rohit Waghchaure
fd8fac7d40 fix: removed non existent patch
(cherry picked from commit c4b3080eae)
2026-03-10 08:56:16 +00:00
Rohit Waghchaure
b7fd9aea6a fix: better validation message for Purchase Invoice with Update Stock
(cherry picked from commit cfb06cf247)
2026-03-10 08:33:50 +00:00
Mihir Kandoi
2b25059315 Merge pull request #53276 from frappe/mergify/bp/version-15-hotfix/pr-53235
fix: update item row delivery dates when header delivery date changes in sales order (backport #53235)
2026-03-10 13:09:31 +05:30
Pandiyan37
dfbb3e97a8 fix(selling): update delivery date in line items
(cherry picked from commit 77367b5517)
2026-03-10 07:23:38 +00:00
ruthra kumar
ee22347d64 Merge pull request #53243 from frappe/mergify/bp/version-15-hotfix/pr-53239
fix: validation for cancellation (backport #53239)
2026-03-10 12:23:14 +05:30
ruthra kumar
4d418d40db Merge pull request #52995 from frappe/mergify/bp/version-15-hotfix/pr-52630
fix(account): compute tax net_amount in JS controller (backport #52630)
2026-03-09 20:26:44 +05:30
ljain112
a6dd07802a fix: enforce permission check for purchase invoice and update test to use service expense account 2026-03-09 19:53:57 +05:30
ljain112
bcc542b1f9 fix(test): include warehouse parameter in asset repair test case 2026-03-09 19:11:02 +05:30
ljain112
ed428ceb1c fix(test): ensure warehouse is consistently referenced in asset repair tests 2026-03-09 18:54:43 +05:30
ljain112
0b1746a4c8 fix: set default repair cost to 0 if no value is returned 2026-03-09 18:34:46 +05:30
ljain112
c71557f432 fix: correct logic for repair cost in asset repair 2026-03-09 18:03:59 +05:30
Rohit Waghchaure
c142a2be9c fix: validation for cancellation
(cherry picked from commit 8de272a8a1)
2026-03-09 07:49:22 +00:00
Mihir Kandoi
3c77653508 Merge pull request #53240 from frappe/mergify/bp/version-15-hotfix/pr-53234
fix(manufacturing): show returned qty in progress bar (backport #53234)
2026-03-09 12:42:04 +05:30
Sudharsanan11
c572a019b4 feat(manufacturing): show disassembled qty in progress bar
(cherry picked from commit ae9ff767fa)
2026-03-09 07:04:32 +00:00
Sudharsanan11
260d87a80c fix(manufacturing): show returned qty in progress bar
(cherry picked from commit 8027f5aafd)
2026-03-09 07:04:31 +00:00
ruthra kumar
e0f5ae2d4c Merge pull request #53228 from frappe/mergify/bp/version-15-hotfix/pr-53227
refactor: party type and party filter for comparison report (backport #53227)
2026-03-08 05:49:51 +05:30
ruthra kumar
37e750e877 refactor: party type and party filter for comparison report
(cherry picked from commit b6f9c0844e)
2026-03-07 12:49:44 +00:00
Frappe PR Bot
1ee03f41f2 chore(release): Bumped to Version 15.100.2
## [15.100.2](https://github.com/frappe/erpnext/compare/v15.100.1...v15.100.2) (2026-03-06)

### Bug Fixes

* stock balance report qty ([9b49a27](9b49a27af6))
2026-03-06 08:35:24 +00:00
rohitwaghchaure
c2f2331d49 Merge pull request #53209 from frappe/mergify/bp/version-15/pr-53207
fix: stock balance report qty (backport #53200) (backport #53207)
2026-03-06 14:04:01 +05:30
rohitwaghchaure
5af5de3315 chore: fix conflicts
(cherry picked from commit 54fdce648e)
2026-03-06 07:38:33 +00:00
Rohit Waghchaure
9b49a27af6 fix: stock balance report qty
(cherry picked from commit a15e5fdc4e)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
(cherry picked from commit 180e232eb0)
2026-03-06 07:38:32 +00:00
rohitwaghchaure
5764f5ec80 Merge pull request #53207 from frappe/mergify/bp/version-15-hotfix/pr-53200
fix: stock balance report qty (backport #53200)
2026-03-06 13:08:11 +05:30
mergify[bot]
e9ae156323 refactor: use postprocess in mapped_doc to update items in subcontracting controller (backport #52724) (#52936)
* refactor: use postprocess in mapped_doc to update items in subcontracting controller

(cherry picked from commit 1d3d09f48c)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: ljain112 <ljain112@gmail.com>
2026-03-06 12:59:15 +05:30
rohitwaghchaure
54fdce648e chore: fix conflicts 2026-03-06 12:47:18 +05:30
Rohit Waghchaure
180e232eb0 fix: stock balance report qty
(cherry picked from commit a15e5fdc4e)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
2026-03-06 07:15:30 +00:00
mergify[bot]
ba4a99b22c fix(help): escape query (backport #53192) (#53194)
fix(help): escape query (#53192)


(cherry picked from commit 702adda000)

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Akhil Narang <me@akhilnarang.dev>
2026-03-05 18:40:49 +05:30
Mihir Kandoi
9100428f1b Merge pull request #53176 from frappe/resolve/version-15-hotfix/pr-53084 2026-03-05 17:36:47 +05:30
Frappe PR Bot
bcc52090c9 chore(release): Bumped to Version 15.100.1
## [15.100.1](https://github.com/frappe/erpnext/compare/v15.100.0...v15.100.1) (2026-03-05)

### Bug Fixes

* balance qty for inv dimension ([68c79a4](68c79a4a79))
2026-03-05 09:43:52 +00:00
rohitwaghchaure
2ca9b75aa6 Merge pull request #53183 from frappe/mergify/bp/version-15/pr-53180
fix: balance qty for inv dimension (backport #52745) (backport #53180)
2026-03-05 14:59:14 +05:30
Rohit Waghchaure
68c79a4a79 fix: balance qty for inv dimension
(cherry picked from commit a3eafe5b18)
(cherry picked from commit 6898d70382)
2026-03-05 08:58:30 +00:00
rohitwaghchaure
1950e82d1e Merge pull request #53180 from frappe/mergify/bp/version-15-hotfix/pr-52745
fix: balance qty for inv dimension (backport #52745)
2026-03-05 14:27:27 +05:30
Rohit Waghchaure
6898d70382 fix: balance qty for inv dimension
(cherry picked from commit a3eafe5b18)
2026-03-05 08:20:02 +00:00
Sudharsanan11
624d1d4759 fix(manufacturing): ignore sales order validation for subassembly item
(cherry picked from commit 6b1aac4aee)
2026-03-05 12:50:27 +05:30
Mihir Kandoi
bf6b5b7b7f Merge pull request #53164 from frappe/mergify/bp/version-15-hotfix/pr-53157
fix: disallow all actions on job card if work order is closed (backport #53157)
2026-03-04 17:09:17 +05:30
Khushi Rawat
24d9e2c5a9 Merge pull request #53162 from frappe/mergify/bp/version-15-hotfix/pr-53154
fix: skip asset sale processing for internal transfer invoices (backport #53154)
2026-03-04 17:00:56 +05:30
Mihir Kandoi
7b2e4832aa fix: disallow all actions on job card if work order is closed
(cherry picked from commit ee19c32c3a)
2026-03-04 11:21:34 +00:00
khushi8112
a7e8f31f56 fix: skip asset sale processing for internal transfer invoices
(cherry picked from commit 9cb3dad079)
2026-03-04 11:07:05 +00:00
Nishka Gosalia
d5a250a254 Merge pull request #53158 from frappe/mergify/bp/version-15-hotfix/pr-53156
fix: updating costing based on employee change in timesheet (backport #53156)
2026-03-04 16:35:06 +05:30
Nishka Gosalia
be598108b6 fix: updating costing based on employee change in timesheet
(cherry picked from commit e37d4a6f7c)
2026-03-04 10:42:50 +00:00
mergify[bot]
68bac20198 Merge pull request #53095 from frappe/mergify/bp/version-15-hotfix/pr-52838
fix: correct fields being updated on material request and purchase or… (backport #52838)
2026-03-04 07:33:02 +00:00
mergify[bot]
072ab8d5f3 feat: allowing rate modification in update item in quotation (backport #53147) (#53150)
Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
2026-03-04 07:29:46 +00:00
Frappe PR Bot
2574c4c18c chore(release): Bumped to Version 15.100.0
# [15.100.0](https://github.com/frappe/erpnext/compare/v15.99.1...v15.100.0) (2026-03-03)

### Bug Fixes

* **accounts receivable:** include invoice payment terms template (backport [#51940](https://github.com/frappe/erpnext/issues/51940)) ([#53105](https://github.com/frappe/erpnext/issues/53105)) ([f5f40db](f5f40dbcc3))
* allow allowed roles to bypass over billing validation ([8e59fe9](8e59fe90cc))
* correct sle voucher_type comparison in get_ref_doctype ([a587b6a](a587b6a57c))
* ensure cache is cleared on fiscal year update and trash ([7278166](7278166b2c))
* ensure contacts are processed only if present in create_prospect_against_crm_deal ([e2b6179](e2b6179b17))
* handle html email template separately in RFQ to avoid jinja context error ([90169a3](90169a39bb))
* item description html validation error ([3c6a120](3c6a120c5c))
* old stock reco entries causing issue in the stock balance report ([b712467](b712467049))
* opening qty in stock balance ([470a9b3](470a9b38b1))
* **payment entry:** round unallocated amount ([76a1907](76a19076bf))
* populate mr owner and set po owner as fallback ([c1f2991](c1f2991694))
* **pricing_rule:** strict validation of `transaction_type` ([5f82db2](5f82db200e))
* resolve conflicts ([6846f02](6846f02cea))
* **selling:** handle selling price validation for FG item ([d5cc51b](d5cc51b426))
* serial no status for Disassemble entry ([8ce541b](8ce541bf46))
* set company based expense account ([74e71f3](74e71f3868))
* **stock:** validate company for receipt documents and expense accounts ([4462088](44620884c1))
* **trial-balance:** totals with filter  `show_group_accounts` enabled ([eabaef2](eabaef2f0b))
* use conversion factor when creating stock entry from pick list ([f2e1482](f2e1482f63))
* use stock qty instead of qty when creating stock entry from MR ([2984f79](2984f79a69))
* use the correct precision value in stock reco ([6d726e4](6d726e4b64))
* validate warehouse of SABB for draft entry ([d5e2515](d5e25153f8))
* Variant Items, List View Enabled to Variant Status Change ([#38468](https://github.com/frappe/erpnext/issues/38468)) ([25b1690](25b169059d))
* voucher detail no in SABB ([6e5738e](6e5738e95c))

### Features

* UOM query filter for opportunity items ([f6a05ec](f6a05ec85e))

### Performance Improvements

* add index on reference_purchase_receipt column ([0766c0e](0766c0ea43))
2026-03-03 17:57:48 +00:00
diptanilsaha
bc9f3a38ce Merge pull request #53142 from frappe/version-15-hotfix 2026-03-03 23:26:05 +05:30
diptanilsaha
f9e5ac6b60 Merge branch 'version-15' into version-15-hotfix 2026-03-03 23:11:48 +05:30
Mihir Kandoi
8131af51a3 Merge pull request #53137 from frappe/mergify/bp/version-15-hotfix/pr-52784
fix(stock): validate company for receipt documents and expense accounts (backport #52784)
2026-03-03 21:46:25 +05:30
Mihir Kandoi
17dc1681b0 Merge pull request #53133 from frappe/mergify/bp/version-15-hotfix/pr-53070
fix: handle html email template separately in RFQ to avoid jinja cont… (backport #53070)
2026-03-03 21:37:21 +05:30
Kavin
62340babb8 Merge pull request #53135 from frappe/mergify/bp/version-15-hotfix/pr-53132
fix(selling): handle selling price validation for FG item  (backport #53132)
2026-03-03 21:31:12 +05:30
Kavin
6846f02cea fix: resolve conflicts 2026-03-03 21:28:34 +05:30
mergify[bot]
10b40836a9 Merge pull request #53128 from frappe/mergify/bp/version-15-hotfix/pr-53037
fix(stock): pass company to avoid document naming rule issue in QI (backport #53037)
2026-03-03 15:56:44 +00:00
Sudharsanan11
9b0d4ab4da test(stock): add test to validate company for receipts and expense accounts
(cherry picked from commit d58171987c)
2026-03-03 15:46:49 +00:00
Sudharsanan11
74e71f3868 fix: set company based expense account
(cherry picked from commit d54d0c25a2)

# Conflicts:
#	erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
2026-03-03 15:46:48 +00:00
Sudharsanan11
44620884c1 fix(stock): validate company for receipt documents and expense accounts
(cherry picked from commit 15dfc08a31)
2026-03-03 15:46:48 +00:00
Mihir Kandoi
bd76ef29cc Merge pull request #53124 from frappe/mergify/bp/version-15-hotfix/pr-53123
fix: serial no status for Disassemble entry (backport #53123)
2026-03-03 21:12:48 +05:30
kavin-114
dced21faf2 test: add unit test for FG Item selling price validation
(cherry picked from commit 723993fdf6)
2026-03-03 15:40:19 +00:00
kavin-114
d5cc51b426 fix(selling): handle selling price validation for FG item
System checks valuation rate in incoming_rate field, since SO has it in valuation_rate field of item row,
need to handle the field name dynamically based upon the doctype name in Selling Controller.

(cherry picked from commit 4335318482)
2026-03-03 15:40:18 +00:00
Mihir Kandoi
3581906033 chore: resolve conflicts 2026-03-03 21:09:03 +05:30
Pugazhendhi Velu
90169a39bb fix: handle html email template separately in RFQ to avoid jinja context error
(cherry picked from commit 49d363b174)

# Conflicts:
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
2026-03-03 15:36:27 +00:00
Mihir Kandoi
86f8bd403d Merge pull request #53126 from frappe/mergify/bp/version-15-hotfix/pr-53099
fix: populate mr owner and set po owner as fallback (backport #53099)
2026-03-03 20:54:40 +05:30
kavin-114
c1f2991694 fix: populate mr owner and set po owner as fallback
(cherry picked from commit bdf4e51da3)
2026-03-03 15:09:12 +00:00
Rohit Waghchaure
8ce541bf46 fix: serial no status for Disassemble entry
(cherry picked from commit 81acefa8ad)
2026-03-03 13:24:03 +00:00
Shariq Ansari
0c0f1a6591 Merge pull request #53121 from frappe/mergify/bp/version-15-hotfix/pr-53119
fix: do not create contact if not passed from crm (backport #53119)
2026-03-03 04:00:02 -08:00
shariquerik
e2b6179b17 fix: ensure contacts are processed only if present in create_prospect_against_crm_deal
(cherry picked from commit 800810d23d)
2026-03-03 11:43:24 +00:00
Mihir Kandoi
96872d314d Merge pull request #53120 from frappe/gh53107 2026-03-03 17:12:33 +05:30
Mihir Kandoi
3c6a120c5c fix: item description html validation error 2026-03-03 16:57:02 +05:30
diptanilsaha
067a57d4c3 Merge pull request #53113 from frappe/mergify/bp/version-15-hotfix/pr-53110
fix(pricing_rule): strict validation of `transaction_type` (backport #53110)
2026-03-03 15:19:30 +05:30
diptanilsaha
814c17aafe refactor: renamed args to pricing_ctx in set_transaction_type for clarity
(cherry picked from commit 6342e78305)
2026-03-03 09:30:32 +00:00
diptanilsaha
5f82db200e fix(pricing_rule): strict validation of transaction_type
(cherry picked from commit 7ec0354a79)
2026-03-03 09:30:32 +00:00
mergify[bot]
f5f40dbcc3 fix(accounts receivable): include invoice payment terms template (backport #51940) (#53105)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-03-03 12:41:03 +05:30
rohitwaghchaure
15054a1c10 Merge pull request #53097 from frappe/mergify/bp/version-15-hotfix/pr-53093
fix: opening qty in stock balance (backport #53093)
2026-03-02 22:42:59 +05:30
rohitwaghchaure
2e26a7fa0b chore: fix linters issue 2026-03-02 21:59:50 +05:30
rohitwaghchaure
08e155cec5 Merge pull request #53091 from frappe/mergify/bp/version-15-hotfix/pr-53087
perf: add index on reference_purchase_receipt column (backport #53087)
2026-03-02 21:57:36 +05:30
rohitwaghchaure
658d219aa1 chore: fix conflicts
Refactor stock balance calculation to use dictionary for stock reconciliation voucher count and improve data handling.
2026-03-02 21:56:38 +05:30
rohitwaghchaure
6f164cb183 chore: fix conflicts 2026-03-02 21:39:36 +05:30
rohitwaghchaure
4568ed3fb4 Merge pull request #53089 from frappe/mergify/bp/version-15-hotfix/pr-53082
fix: validate warehouse of SABB for draft entry (backport #53082)
2026-03-02 21:37:47 +05:30
Rohit Waghchaure
470a9b38b1 fix: opening qty in stock balance
(cherry picked from commit d7fdab99cb)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
2026-03-02 15:19:30 +00:00
kavin-114
0766c0ea43 perf: add index on reference_purchase_receipt column
(cherry picked from commit 8c94396ad9)

# Conflicts:
#	erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
2026-03-02 09:08:16 +00:00
rohitwaghchaure
9d73faddfe chore: fix conflicts
Removed redundant on_update method and cleaned up code.
2026-03-02 14:35:45 +05:30
Rohit Waghchaure
d5e25153f8 fix: validate warehouse of SABB for draft entry
(cherry picked from commit 9b8f685c82)

# Conflicts:
#	erpnext/controllers/stock_controller.py
2026-03-02 08:52:37 +00:00
rohitwaghchaure
94afc32a8d Merge pull request #53076 from frappe/mergify/bp/version-15-hotfix/pr-53050
fix: voucher detail no in SABB (backport #53050)
2026-03-02 12:48:03 +05:30
rohitwaghchaure
97df39a630 chore: fix conflicts 2026-03-02 12:10:43 +05:30
Rohit Waghchaure
6e5738e95c fix: voucher detail no in SABB
(cherry picked from commit c37a56ec89)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
2026-03-02 04:23:42 +00:00
Mihir Kandoi
6147541f58 Merge pull request #53068 from frappe/mergify/bp/version-15-hotfix/pr-53067
fix: use the correct precision value in stock reco (backport #53067)
2026-03-01 21:36:56 +05:30
Mihir Kandoi
6d726e4b64 fix: use the correct precision value in stock reco
(cherry picked from commit 36726b0f7b)
2026-03-01 15:52:02 +00:00
Mihir Kandoi
8fed606357 Merge pull request #53065 from frappe/mergify/bp/version-15-hotfix/pr-53062
fix: correct sle voucher_type comparison in get_ref_doctype (backport #53062)
2026-03-01 21:01:39 +05:30
Sanjesh
a587b6a57c fix: correct sle voucher_type comparison in get_ref_doctype
(cherry picked from commit cffb59ae73)
2026-03-01 15:16:29 +00:00
Mihir Kandoi
51fb7affeb Merge pull request #53060 from frappe/mergify/bp/version-15-hotfix/pr-53057
fix: allow allowed roles to bypass over billing validation (backport #53057)
2026-03-01 20:43:16 +05:30
Mihir Kandoi
6ac2f510a6 chore: resolve conflicts 2026-03-01 20:28:00 +05:30
Mihir Kandoi
2a78f47f02 Merge pull request #53058 from frappe/mergify/bp/version-15-hotfix/pr-53052
fix: use stock qty instead of qty when creating stock entry from MR (backport #53052)
2026-03-01 15:52:19 +05:30
Mihir Kandoi
8e59fe90cc fix: allow allowed roles to bypass over billing validation
(cherry picked from commit 04127019f9)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
2026-03-01 10:18:05 +00:00
Mihir Kandoi
bf7b9c10dc Merge pull request #53053 from frappe/mergify/bp/version-15-hotfix/pr-53051
fix: use conversion factor when creating stock entry from pick list (backport #53051)
2026-03-01 15:25:57 +05:30
Mihir Kandoi
2984f79a69 fix: use stock qty instead of qty when creating stock entry from MR
(cherry picked from commit 30c3ff2efe)
2026-03-01 09:51:35 +00:00
Mihir Kandoi
f2e1482f63 fix: use conversion factor when creating stock entry from pick list
(cherry picked from commit 5f12b0db3f)
2026-03-01 09:40:21 +00:00
Diptanil Saha
0458930d9e Merge pull request #53019 from diptanilsaha/tb_total 2026-02-27 16:52:46 +05:30
Frappe PR Bot
b2a8af5ba6 chore(release): Bumped to Version 15.99.1
## [15.99.1](https://github.com/frappe/erpnext/compare/v15.99.0...v15.99.1) (2026-02-27)

### Bug Fixes

* old stock reco entries causing issue in the stock balance report ([bba0a6d](bba0a6d950))
2026-02-27 10:50:36 +00:00
rohitwaghchaure
45eed5fd81 Merge pull request #53025 from frappe/mergify/bp/version-15/pr-53020
fix: old stock reco entries causing issue in the stock balance report (backport #53013) (backport #53020)
2026-02-27 16:18:40 +05:30
rohitwaghchaure
435c852b54 chore: fix conflicts
Added a method to prepare stock reconciliation voucher-wise count and updated logic for handling stock reconciliation entries.

(cherry picked from commit cc6fbde463)
2026-02-27 10:32:36 +00:00
Rohit Waghchaure
bba0a6d950 fix: old stock reco entries causing issue in the stock balance report
(cherry picked from commit 0874cbc268)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
(cherry picked from commit b712467049)
2026-02-27 10:32:36 +00:00
rohitwaghchaure
51d5885454 Merge pull request #53020 from frappe/mergify/bp/version-15-hotfix/pr-53013
fix: old stock reco entries causing issue in the stock balance report (backport #53013)
2026-02-27 16:02:12 +05:30
rohitwaghchaure
cc6fbde463 chore: fix conflicts
Added a method to prepare stock reconciliation voucher-wise count and updated logic for handling stock reconciliation entries.
2026-02-27 15:46:52 +05:30
Rohit Waghchaure
b712467049 fix: old stock reco entries causing issue in the stock balance report
(cherry picked from commit 0874cbc268)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
2026-02-27 09:26:37 +00:00
diptanilsaha
eabaef2f0b fix(trial-balance): totals with filter show_group_accounts enabled 2026-02-27 14:42:53 +05:30
Mihir Kandoi
f90e984833 Merge pull request #53017 from frappe/mergify/bp/version-15-hotfix/pr-53012 2026-02-27 13:58:02 +05:30
Thomas antony
f6a05ec85e feat: UOM query filter for opportunity items
Add UOM query filter based on item code in opportunity form.

(cherry picked from commit b33f06701c)
2026-02-27 08:23:31 +00:00
Diptanil Saha
5ba38bcf32 Merge pull request #53001 from frappe/mergify/bp/version-15-hotfix/pr-38468
fix: Variant Items, List View Enabled to Variant Status Change (backport #38468)
2026-02-26 16:50:55 +05:30
diptanilsaha
39f8d35414 chore: resolve conflict 2026-02-26 16:44:17 +05:30
Parameshwari Palanisamy
25b169059d fix: Variant Items, List View Enabled to Variant Status Change (#38468)
* feat: Purchase Register Report Supplier Group filter Adding

* fix: Variant Items, List View Enabled to Variant Status Change

* Update purchase_register.js

* Update purchase_register.py

(cherry picked from commit 9ea963bfe9)

# Conflicts:
#	erpnext/stock/doctype/item/item_list.js
2026-02-26 11:08:55 +00:00
David Arnold
717c5b25eb fix: client-side taxes calculation (#44510)
closes: #44328
2026-02-26 13:55:17 +05:30
ruthra kumar
e5282a48ae chore: resolve conflicts 2026-02-26 13:52:23 +05:30
Luis Mendoza
516ad9021b fix(accounts): round and convert net_amount to company currency in JS tax controller
(cherry picked from commit b10b205394)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2026-02-26 08:17:52 +00:00
Luis Mendoza
86c628521e style: prettier formatting
(cherry picked from commit 485166b668)
2026-02-26 08:17:51 +00:00
Luis Mendoza
6ad84d66cc fix(accounts): compute tax net_amount in JS controller
(cherry picked from commit 153ad99f85)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2026-02-26 08:17:51 +00:00
ruthra kumar
a2a6460c6c Merge pull request #52992 from frappe/mergify/bp/version-15-hotfix/pr-52188
fix(payment entry): round unallocated amount (backport #52188)
2026-02-26 13:24:06 +05:30
ravibharathi656
76a19076bf fix(payment entry): round unallocated amount
(cherry picked from commit b0d6751777)
2026-02-26 07:38:07 +00:00
ruthra kumar
fdaa0b8b58 Merge pull request #52974 from frappe/mergify/bp/version-15-hotfix/pr-52973
fix: ensure cache is cleared on fiscal year update and trash (backport #52973)
2026-02-26 10:00:29 +05:30
ljain112
7278166b2c fix: ensure cache is cleared on fiscal year update and trash
(cherry picked from commit 39b0e3522a)
2026-02-25 14:35:48 +00:00
mergify[bot]
9657c6431d Merge pull request #52932 from frappe/mergify/bp/version-15-hotfix/pr-52824
refactor: separate construction of chart related data from `get_columns()` (backport #52824)
2026-02-25 15:11:46 +05:30
Frappe PR Bot
7ad770a83a chore(release): Bumped to Version 15.99.0
# [15.99.0](https://github.com/frappe/erpnext/compare/v15.98.1...v15.99.0) (2026-02-25)

### Bug Fixes

* **`fiscal_year_company`:** made `company` field mandatory ([2fffc94](2fffc9448b))
* **`fiscal_year`:** `Fiscal Year` auto-generation and notification ([397f39e](397f39e271))
* `update_stock` behaviour on selling invoices ([82bcb62](82bcb62b21))
* Add handling for Sales Invoice Item quantity field ([41c7890](41c7890a6d))
* add purchase invoice as well ([2fc3e30](2fc3e30f9f))
* avoid duplicate taxes and charges rows in payment entry (backport [#52178](https://github.com/frappe/erpnext/issues/52178)) ([#52318](https://github.com/frappe/erpnext/issues/52318)) ([946c355](946c3554b1))
* better permissions on make payment request ([ce7101f](ce7101f555))
* bug with comparison regarding `None` values and empty string ([852c200](852c200ee0))
* check gl account of an associated bank account in bank transaction ([6b286ae](6b286ae03d))
* enfore permission on make_payment_request ([4602919](460291990a))
* get employee email with priority if preferred is not set ([943e2c0](943e2c00bc))
* ignore permissions instead of saving parent ([bce77b6](bce77b6117))
* inconsistent label name between parent and child ([1bf608f](1bf608f835))
* **manufacturing:** remove delete query of job card & batch and serial no  ([#52840](https://github.com/frappe/erpnext/issues/52840)) ([e30b2f1](e30b2f1d04))
* **manufacturing:** set pick list purpose while creating it from work order ([33d48c5](33d48c5575))
* **manufacturing:** update status for work order before calculating planned qty ([b3bcfd5](b3bcfd5a64))
* permission issue for quotation item during update item ([5a3c027](5a3c027432))
* prevent precision errors in  discount distribution with inclusive tax ([61ac180](61ac18069b))
* **Purchase Receipt:** copy project from first row when adding items ([fd48fb4](fd48fb49b9))
* remove supplier invoice date/posting date validation ([dcf4ac6](dcf4ac66bb))
* reservation based on field should be read only in SRE ([c3626d6](c3626d67ca))
* restore missing  `has_permission` import ([0ba965a](0ba965aae6))
* **sales-order:** update quotation status while cancelling sales order ([#52822](https://github.com/frappe/erpnext/issues/52822)) ([2420122](2420122f0e))
* **sales-order:** update quotation status while cancelling sales order (backport [#52822](https://github.com/frappe/erpnext/issues/52822)) ([#52918](https://github.com/frappe/erpnext/issues/52918)) ([3ae5de7](3ae5de7b11))
* sensible insufficient stock message in pick list ([3bafa36](3bafa360b2))
* setup fails to set abbr to departments ([c432506](c432506912))
* skip empty dimension values in exchange gain loss ([09ba980](09ba9808de))
* typo ([3893900](38939005ca))
* unable to submit subcontracting order if created from material request ([0422117](0422117003))
* update items fetches wrong item code ([97a4a5f](97a4a5f1cc))
* **work_order:** update returned qty ([bb1a655](bb1a655efb))

### Features

* **Journal Entry Account:** add Bank Transaction as Reference Type (backport [#52760](https://github.com/frappe/erpnext/issues/52760)) ([#52815](https://github.com/frappe/erpnext/issues/52815)) ([7032197](7032197f97))
* retrieve employee basic contact information ([4b2ac62](4b2ac626c5))
* retrieve employee contact details ([caa03ef](caa03efbe1))
* update item button addition for quotation (backport [#50976](https://github.com/frappe/erpnext/issues/50976)) ([#52810](https://github.com/frappe/erpnext/issues/52810)) ([800e384](800e38453b))
* update item button addition for quotation (backport [#50976](https://github.com/frappe/erpnext/issues/50976)) ([#52810](https://github.com/frappe/erpnext/issues/52810)) ([e2a1a7a](e2a1a7a36d))

### Reverts

* Revert "feat: update item button addition for quotation (backport [#50976](https://github.com/frappe/erpnext/issues/50976)) ([#5](https://github.com/frappe/erpnext/issues/5)…" ([656b1bc](656b1bcede))
2026-02-25 06:29:35 +00:00
ruthra kumar
5396b141dd Merge pull request #52926 from frappe/version-15-hotfix
chore: release v15
2026-02-25 11:58:05 +05:30
Mihir Kandoi
4ffb161ec8 Merge pull request #52943 from frappe/mergify/bp/version-15-hotfix/pr-52942 2026-02-25 10:30:10 +05:30
Mihir Kandoi
b37fc6676e chore: clearer description for internal transfer at arms length
(cherry picked from commit bd9e5e97d7)
2026-02-25 04:43:07 +00:00
ruthra kumar
965fcf8b71 Merge pull request #52928 from frappe/mergify/bp/version-15-hotfix/pr-52029
fix: prevent precision errors in  discount distribution with inclusive tax (backport #52029)
2026-02-24 18:39:37 +05:30
ljain112
61ac18069b fix: prevent precision errors in discount distribution with inclusive tax
(cherry picked from commit 2068299766)
2026-02-24 11:28:44 +00:00
ruthra kumar
3ae5de7b11 fix(sales-order): update quotation status while cancelling sales order (backport #52822) (#52918)
fix(sales-order): update quotation status while cancelling sales order (#52822)

* fix(sales-order): update quotation status while cancelling sales order

* test: validate quotation status

* chore: remove submit

(cherry picked from commit d638f3e033)

Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com>
2026-02-24 12:08:25 +05:30
Sowmya
2420122f0e fix(sales-order): update quotation status while cancelling sales order (#52822)
* fix(sales-order): update quotation status while cancelling sales order

* test: validate quotation status

* chore: remove submit

(cherry picked from commit d638f3e033)
2026-02-24 06:18:23 +00:00
ruthra kumar
c5826f4132 Merge pull request #52907 from frappe/mergify/bp/version-15-hotfix/pr-52896
fix: skip empty dimension values in exchange gain loss (backport #52896)
2026-02-23 21:22:02 +05:30
ravibharathi656
09ba9808de fix: skip empty dimension values in exchange gain loss
(cherry picked from commit 7df9d951c6)
2026-02-23 15:36:29 +00:00
Imesha Sudasingha
71248ff40b Merge pull request #52544 from one-highflyer/fix/improve-reserved-serial-no-error-message
fix(stock): improve error message when serial no is reserved via SRE
2026-02-23 15:12:08 +00:00
Diptanil Saha
bfe0443530 Merge pull request #52899 from frappe/mergify/bp/version-15-hotfix/pr-50301 2026-02-23 20:38:49 +05:30
mergify[bot]
eda4462e5f Merge pull request #52722 from frappe/mergify/bp/version-15-hotfix/pr-52720
fix: wrong display_depends_on condition for item group and brand chil… (backport #52720)
2026-02-23 15:04:41 +00:00
diptanilsaha
0ba965aae6 fix: restore missing has_permission import 2026-02-23 20:07:27 +05:30
mergify[bot]
07eb5c714a Merge pull request #52897 from frappe/mergify/bp/version-15-hotfix/pr-52878
fix: standalone sales invoice return should not fallback to item mast… (backport #52878)
2026-02-23 13:54:53 +00:00
Mihir Kandoi
245bc7d2fb Merge pull request #52886 from frappe/mergify/bp/version-15-hotfix/pr-52840
fix(manufacturing): remove delete query of job card & batch and serial no  (backport #52840)
2026-02-23 19:16:45 +05:30
Abdeali Chharchhoda
773e56808a refactor: method to get employee contact without permission check
(cherry picked from commit 58cdb9503b)
2026-02-23 13:33:39 +00:00
Abdeali Chharchhoda
cb17dbd616 refactor: use common method to get employee contacts
(cherry picked from commit ec1eb6d222)
2026-02-23 13:33:39 +00:00
Abdeali Chharchhoda
943e2c00bc fix: get employee email with priority if preferred is not set
(cherry picked from commit 7b89c12470)
2026-02-23 13:33:39 +00:00
Abdeali Chharchhoda
0866c03e20 refactor: add validation for missing employee parameter
(cherry picked from commit b8e06b9636)
2026-02-23 13:33:38 +00:00
Abdeali Chharchhoda
4078e252c2 refactor: fetch employee contact details in realtime
(cherry picked from commit 2ea6508fa5)
2026-02-23 13:33:38 +00:00
Abdeali Chharchhoda
caa03efbe1 feat: retrieve employee contact details
(cherry picked from commit a41297d841)
2026-02-23 13:33:37 +00:00
Abdeali Chharchhoda
4b2ac626c5 feat: retrieve employee basic contact information
(cherry picked from commit 4ad1474e32)
2026-02-23 13:33:37 +00:00
Abdeali Chharchhoda
ae733cd7ad chore: Removing unused import
(cherry picked from commit 87c59f471c)
2026-02-23 13:33:37 +00:00
ruthra kumar
cec539629b Merge pull request #52894 from frappe/mergify/bp/version-15-hotfix/pr-52812
fix: bank account mismatch error on reverse transaction reconciliation (backport #52812)
2026-02-23 18:31:05 +05:30
ervishnucs
6b286ae03d fix: check gl account of an associated bank account in bank transaction
(cherry picked from commit 8fe0bf4ba3)
2026-02-23 12:36:25 +00:00
Sudharsanan Ashok
e30b2f1d04 fix(manufacturing): remove delete query of job card & batch and serial no (#52840)
* fix(manufacturing): remove delete query of batch and serial no

* fix(manufacturing): remove delete query of job card

* fix: remove delete function call for work order

(cherry picked from commit 8b2a971019)
2026-02-23 11:29:18 +00:00
Mihir Kandoi
d59d30c50a Merge pull request #52882 from frappe/mergify/bp/version-15-hotfix/pr-52880
fix(work_order): update returned qty on work order (backport #52880)
2026-02-23 15:48:35 +05:30
mergify[bot]
946c3554b1 fix: avoid duplicate taxes and charges rows in payment entry (backport #52178) (#52318)
Co-authored-by: Dharanidharan S <dharanidharans1328@gmail.com>
fix: avoid duplicate taxes and charges rows in payment entry (#52178)
2026-02-23 10:15:00 +00:00
Pandiyan37
bb1a655efb fix(work_order): update returned qty
(cherry picked from commit b7f45e6963)
2026-02-23 10:03:53 +00:00
mergify[bot]
dada7c4aa8 Merge pull request #52873 from frappe/mergify/bp/version-15-hotfix/pr-52871
fix: use stock qty instead of qty when updating transferred qty in WO (backport #52871)
2026-02-23 09:43:11 +00:00
Mihir Kandoi
dabf2c7027 Merge pull request #52862 from frappe/mergify/bp/version-15-hotfix/pr-52803
fix(manufacturing): update closed status for current work order before calculating planned qty (backport #52803)
2026-02-23 07:46:07 +05:30
Sudharsanan11
76760c2ee3 test(manufacturing): add test to validate the planned qty
(cherry picked from commit cfbdfcf515)
2026-02-23 02:01:23 +00:00
Sudharsanan11
b3bcfd5a64 fix(manufacturing): update status for work order before calculating planned qty
(cherry picked from commit 4d40c84a31)
2026-02-23 02:01:22 +00:00
Diptanil Saha
377435fa7d Merge pull request #52854 from diptanilsaha/us_si_pi 2026-02-22 19:48:02 +05:30
diptanilsaha
82bcb62b21 fix: update_stock behaviour on selling invoices 2026-02-22 04:20:31 +05:30
mergify[bot]
7032197f97 feat(Journal Entry Account): add Bank Transaction as Reference Type (backport #52760) (#52815)
* feat: add Bank Transaction as Reference Type to Journal Entry Account (#52760)

* feat: add Bank Transaction as Reference Type to Journal Entry Account

* fix: take care of existing property setters

* fix: cancelling Bank Transactions should still be possible

* fix: handle blank options in patch

* fix: hide Reference Due Date for Bank Transaction

(cherry picked from commit 387fb1b202)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-02-21 15:43:26 +01:00
Diptanil Saha
ef801d6bb4 Merge pull request #52849 from frappe/mergify/bp/version-15-hotfix/pr-52842
refactor: `Fiscal Year` cleanup (backport #52842)
2026-02-21 17:05:13 +05:30
diptanilsaha
bb8e5adadc chore: resolve conflicts 2026-02-21 16:36:58 +05:30
diptanilsaha
397f39e271 fix(fiscal_year): Fiscal Year auto-generation and notification
(cherry picked from commit 4c76786ce4)

# Conflicts:
#	erpnext/accounts/doctype/fiscal_year/fiscal_year.py
#	erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.html
#	erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.json
2026-02-21 07:41:21 +00:00
diptanilsaha
2fffc9448b fix(fiscal_year_company): made company field mandatory
(cherry picked from commit 94fb7e11b4)

# Conflicts:
#	erpnext/accounts/doctype/fiscal_year_company/fiscal_year_company.json
2026-02-21 07:41:21 +00:00
diptanilsaha
5a1c61f4d9 refactor: Fiscal Year DocType cleanup
(cherry picked from commit 74ac28fc70)

# Conflicts:
#	erpnext/accounts/doctype/fiscal_year/fiscal_year.py
2026-02-21 07:41:20 +00:00
Mihir Kandoi
266658cda1 Merge pull request #52846 from frappe/mergify/bp/version-15-hotfix/pr-52845 2026-02-21 12:40:11 +05:30
Mihir Kandoi
dcf4ac66bb fix: remove supplier invoice date/posting date validation
(cherry picked from commit 7cff0ba626)
2026-02-21 06:55:08 +00:00
Mihir Kandoi
813f0fa8c9 Merge pull request #52836 from frappe/mergify/bp/version-15-hotfix/pr-52835
fix: inconsistent label name between parent and child (backport #52835)
2026-02-20 17:17:55 +05:30
Mihir Kandoi
58d9694173 chore: resolve conflicts 2026-02-20 17:01:17 +05:30
Mihir Kandoi
1bf608f835 fix: inconsistent label name between parent and child
(cherry picked from commit d6e1ca0f10)

# Conflicts:
#	erpnext/selling/doctype/sales_order_item/sales_order_item.json
2026-02-20 11:28:35 +00:00
Mihir Kandoi
5eb4baccd6 Merge pull request #52826 from frappe/mergify/bp/version-15-hotfix/pr-52821
fix: sensible insufficient stock message in pick list (backport #52821)
2026-02-20 14:39:00 +05:30
Mihir Kandoi
ae0acd9ba1 Merge pull request #52828 from frappe/mergify/bp/version-15-hotfix/pr-52825
fix: update items fetches wrong item code (backport #52825)
2026-02-20 14:26:37 +05:30
Mihir Kandoi
97a4a5f1cc fix: update items fetches wrong item code
(cherry picked from commit ba96d37c11)
2026-02-20 08:54:14 +00:00
Mihir Kandoi
3bafa360b2 fix: sensible insufficient stock message in pick list
(cherry picked from commit 1352dc79bb)
2026-02-20 08:53:36 +00:00
Mihir Kandoi
de571c9266 Merge pull request #52817 from mihir-kandoi/gh52069
feat: update item button addition for quotation (backport #50976)
2026-02-19 23:30:52 +05:30
Mihir Kandoi
e7e67902e9 Merge pull request #52818 from frappe/mergify/bp/version-15-hotfix/pr-52811
fix: permission issue for quotation item during update item (backport #52811)
2026-02-19 23:19:42 +05:30
mergify[bot]
800e38453b feat: update item button addition for quotation (backport #50976) (#52810)
* feat: update item button addition for quotation (#50976)

* feat: update item button addition for quotation

* feat: update item button addition for supplier quotation

* fix: test case

---------

Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit f4c0611cc5)

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* 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-02-19 23:11:32 +05:30
Mihir Kandoi
38939005ca fix: typo
(cherry picked from commit 732c98b72f)
2026-02-19 17:28:31 +00:00
Mihir Kandoi
bce77b6117 fix: ignore permissions instead of saving parent
(cherry picked from commit 6342e9a3e2)
2026-02-19 17:28:31 +00:00
Nishka Gosalia
5a3c027432 fix: permission issue for quotation item during update item
(cherry picked from commit 58b8af0fa8)
2026-02-19 17:28:30 +00:00
Mihir Kandoi
ee7602e752 Merge pull request #52813 from frappe/revert-52810-mergify/bp/version-15-hotfix/pr-50976
revert: "feat: update item button addition for quotation"
2026-02-19 20:39:43 +05:30
Mihir Kandoi
656b1bcede Revert "feat: update item button addition for quotation (backport #50976) (#5…"
This reverts commit e2a1a7a36d.
2026-02-19 20:22:25 +05:30
mergify[bot]
e2a1a7a36d feat: update item button addition for quotation (backport #50976) (#52810)
* feat: update item button addition for quotation (#50976)

* feat: update item button addition for quotation

* feat: update item button addition for supplier quotation

* fix: test case

---------

Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit f4c0611cc5)

# Conflicts:
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
#	erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
#	erpnext/controllers/accounts_controller.py
#	erpnext/selling/doctype/quotation/test_quotation.py

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* 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-02-19 14:41:01 +00:00
Mihir Kandoi
283b118773 Merge pull request #52805 from frappe/mergify/bp/version-15-hotfix/pr-52804
fix(Purchase Receipt): copy project from first row when adding items (backport #52804)
2026-02-19 14:03:34 +05:30
Marc Ramser
fd48fb49b9 fix(Purchase Receipt): copy project from first row when adding items
Adds `items_add` method to copy expense_account, cost_center and project from first row to newly added items, matching Purchase Invoice behavior.

(cherry picked from commit 21423676c9)
2026-02-19 08:31:59 +00:00
Mihir Kandoi
a57a9d7548 Merge pull request #52795 from frappe/mergify/bp/version-15-hotfix/pr-52794
fix: reservation based on field should be read only in SRE (backport #52794)
2026-02-19 12:56:57 +05:30
Mihir Kandoi
49dca6016c Merge pull request #52797 from frappe/mergify/bp/version-15-hotfix/pr-52792
fix: unable to submit subcontracting order if created from material r… (backport #52792)
2026-02-19 12:56:45 +05:30
Mihir Kandoi
0422117003 fix: unable to submit subcontracting order if created from material request
(cherry picked from commit 37323480dd)
2026-02-19 05:39:34 +00:00
Mihir Kandoi
6d32089d9e chore: resolve conflicts 2026-02-19 10:51:40 +05:30
Mihir Kandoi
c3626d67ca fix: reservation based on field should be read only in SRE
(cherry picked from commit 21452b4c6e)

# Conflicts:
#	erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
2026-02-19 05:10:25 +00:00
Frappe PR Bot
2888b68334 chore(release): Bumped to Version 15.98.1
## [15.98.1](https://github.com/frappe/erpnext/compare/v15.98.0...v15.98.1) (2026-02-19)

### Bug Fixes

* better permissions on make payment request ([f06e20d](f06e20d993))
2026-02-19 05:00:53 +00:00
ruthra kumar
f7f10a5930 Merge pull request #52789 from frappe/mergify/bp/version-15/pr-52763
fix: better permissions on make payment request (backport #52763)
2026-02-19 10:27:15 +05:30
Mihir Kandoi
3fcda3a414 Merge pull request #52787 from frappe/mergify/bp/version-15-hotfix/pr-52490
fix: Add handling for Sales Invoice Item quantity field (backport #52490)
2026-02-19 10:15:30 +05:30
ruthra kumar
f06e20d993 fix: better permissions on make payment request
(cherry picked from commit f36962fc58)
2026-02-19 04:31:56 +00:00
Mihir Kandoi
2fc3e30f9f fix: add purchase invoice as well
(cherry picked from commit 1fc2eddf6f)
2026-02-19 04:12:47 +00:00
Thomas antony
41c7890a6d fix: Add handling for Sales Invoice Item quantity field
Add handling for Sales Invoice Item quantity field

(cherry picked from commit edfcaee99b)
2026-02-19 04:12:46 +00:00
mergify[bot]
d9f1b0be77 Merge pull request #52785 from frappe/mergify/bp/version-15-hotfix/pr-52712
fix: addresses portal (backport #52712)
2026-02-19 04:08:56 +00:00
Mihir Kandoi
fd7f6fd7f3 Merge pull request #52774 from frappe/mergify/bp/version-15-hotfix/pr-52628
fix(manufacturing): set pick list purpose while creating it from work order (backport #52628)
2026-02-18 15:30:45 +05:30
Sudharsanan11
33d48c5575 fix(manufacturing): set pick list purpose while creating it from work order
(cherry picked from commit 23ccc2a8c5)
2026-02-18 09:45:36 +00:00
Sagar Vora
f7332258e7 Merge pull request #52773 from frappe/revert-52366-mergify/bp/version-15-hotfix/pr-52279
revert: "fix(profit and loss statement): exclude non period columns"
2026-02-18 08:34:13 +00:00
Sagar Vora
d782c52b76 Revert "fix(profit and loss statement): exclude non period columns (backport #52279)" 2026-02-18 13:52:50 +05:30
ruthra kumar
be4fe38998 Merge pull request #52767 from frappe/mergify/bp/version-15-hotfix/pr-52763
fix: better permissions on make payment request (backport #52763)
2026-02-18 12:44:32 +05:30
ruthra kumar
ce7101f555 fix: better permissions on make payment request
(cherry picked from commit f36962fc58)
2026-02-18 06:58:27 +00:00
ruthra kumar
56dd37c9e6 Merge pull request #52765 from frappe/mergify/bp/version-15-hotfix/pr-52419
fix: enfore permission on make_payment_request (backport #52419)
2026-02-18 12:23:29 +05:30
ruthra kumar
460291990a fix: enfore permission on make_payment_request
(cherry picked from commit b755ca12ca)
2026-02-18 06:32:29 +00:00
Mihir Kandoi
80976ae466 Merge pull request #52753 from frappe/mergify/bp/version-15-hotfix/pr-52750
fix: setup fails to set abbr to departments (backport #52750)
2026-02-17 21:47:32 +05:30
Mihir Kandoi
9a28d4ef5a Merge pull request #52751 from frappe/mergify/bp/version-15-hotfix/pr-52743
bug: fix comparison regarding `None` values (backport #52743)
2026-02-17 21:37:25 +05:30
Mihir Kandoi
c432506912 fix: setup fails to set abbr to departments
(cherry picked from commit debe868950)
2026-02-17 15:57:32 +00:00
Markus Lobedann
852c200ee0 fix: bug with comparison regarding None values and empty string
In their default state, the fields can be `None`. When a user enters something and deletes it afterwards, the fields contain an empty string.

This fixes the comparison.

(cherry picked from commit 3fd5a0f100)
2026-02-17 15:51:48 +00:00
Frappe PR Bot
a66854d16d chore(release): Bumped to Version 15.98.0
# [15.98.0](https://github.com/frappe/erpnext/compare/v15.97.0...v15.98.0) (2026-02-17)

### Bug Fixes

* **accounts-controller:** handle empty items list ([13239a9](13239a9dee))
* **accounts:** correct base grand total and rounded total mismatch ([#51739](https://github.com/frappe/erpnext/issues/51739)) ([8bdbb24](8bdbb24d73))
* add base_tax_withholding_net_total to tax withholding report ([ed42d54](ed42d54989))
* allow rename for market segment doctype ([0a41987](0a4198718b))
* allow sequence id edit in BOM if routing is not set ([c425944](c425944bdf))
* better validation for negative batch ([85d18fa](85d18fa7a4))
* cancel SABB if SLE cancelled from LCV ([f2a77d1](f2a77d178d))
* consider sle for negative stock validation ([ca79f64](ca79f6478a))
* do not allow plant floor company and warehouse to be updated ([d6333c1](d6333c1562))
* **manufacturing:** add sales order fields in subassembly child table ([0576752](0576752d3b))
* **manufacturing:** set sales order references in subassembly child table ([53e18a9](53e18a9beb))
* Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled ([78a3701](78a3701f4c))
* **pos_invoice:** add correct depends on condition (backport [#52689](https://github.com/frappe/erpnext/issues/52689)) ([#52693](https://github.com/frappe/erpnext/issues/52693)) ([4fe9689](4fe968961a))
* **postgres:** validate against period closing using MAX(period_end_date) ([#51554](https://github.com/frappe/erpnext/issues/51554)) ([9ec3031](9ec30319e4))
* production plan status ([97a6610](97a6610c0c))
* recalculate tax withholding during Purchase Order child update ([273029d](273029d0f0))
* set base_tax_withholding_net_total for jv in tds report ([68099a9](68099a9b5c))
* standalone credit/debit notes should not fetch any serial or batch by default ([79c3bc9](79c3bc9bcd))
* total weight does not update when updating items ([e12871b](e12871b408))

### Features

* Negative Batch report ([6313636](631363632b))
* show formatted currency symbol on ledger preview ([383648f](383648fb59))
2026-02-17 14:12:38 +00:00
ruthra kumar
72bb3e26cd Merge pull request #52730 from frappe/version-15-hotfix
chore: release v15
2026-02-17 19:38:01 +05:30
rohitwaghchaure
128c2bf8b9 Merge pull request #52739 from frappe/mergify/bp/version-15-hotfix/pr-52729
feat: Negative Batch report (backport #52729)
2026-02-17 17:17:42 +05:30
Mihir Kandoi
7aa46af0c3 Merge pull request #52735 from frappe/mergify/bp/version-15-hotfix/pr-52733
fix: allow sequence ID edit in BOM if routing is not set (backport #52733)
2026-02-17 16:39:01 +05:30
Rohit Waghchaure
631363632b feat: Negative Batch report
(cherry picked from commit 34edbed00b)
2026-02-17 11:04:17 +00:00
Mihir Kandoi
3e3c489178 Merge pull request #52737 from frappe/mergify/bp/version-15-hotfix/pr-52677
fix: standalone credit/debit notes should not fetch any serial or bat… (backport #52677)
2026-02-17 16:25:58 +05:30
Mihir Kandoi
79c3bc9bcd fix: standalone credit/debit notes should not fetch any serial or batch by default
(cherry picked from commit 2017edca88)
2026-02-17 10:40:08 +00:00
Mihir Kandoi
c6682f130c chore: resolve conflicts 2026-02-17 16:07:49 +05:30
Mihir Kandoi
c425944bdf fix: allow sequence id edit in BOM if routing is not set
(cherry picked from commit 08529964b4)

# Conflicts:
#	erpnext/manufacturing/doctype/bom_operation/bom_operation.json
2026-02-17 10:35:58 +00:00
Mihir Kandoi
0935b181bf Merge pull request #52721 from aerele/backport-52626
fix(manufacturing): add sales order fields in subassembly child table (backport #52626)
2026-02-17 13:42:26 +05:30
ruthra kumar
bc50b94a87 Merge pull request #52595 from ljain112/fix-tds-report-v15
fix: add base_tax_withholding_net_total to tax withholding report
2026-02-17 13:39:48 +05:30
Mihir Kandoi
02f16715e0 Merge pull request #52717 from frappe/mergify/bp/version-15-hotfix/pr-52716
fix: do not allow plant floor company and warehouse to be updated (backport #52716)
2026-02-17 12:42:30 +05:30
ruthra kumar
f0ab9b6e76 Merge pull request #52151 from aerele/fix-po-tax-withholding
fix(purchase order): re-calculate tax withholding during update items
2026-02-17 12:19:06 +05:30
Pandiyan37
d2dc0a4c9a test(manufacturing): add test to validate the sales order references for sub assembly items 2026-02-17 12:15:33 +05:30
Mihir Kandoi
e8b46d9815 chore: resolve conflicts 2026-02-17 12:13:33 +05:30
Mihir Kandoi
d6333c1562 fix: do not allow plant floor company and warehouse to be updated
(cherry picked from commit fd72132743)

# Conflicts:
#	erpnext/manufacturing/doctype/plant_floor/plant_floor.json
2026-02-17 06:42:15 +00:00
Pandiyan37
53e18a9beb fix(manufacturing): set sales order references in subassembly child table 2026-02-17 12:11:05 +05:30
Pandiyan37
0576752d3b fix(manufacturing): add sales order fields in subassembly child table 2026-02-17 12:10:08 +05:30
Mihir Kandoi
e3a2b310d8 Merge pull request #52714 from frappe/mergify/bp/version-15-hotfix/pr-52713
fix: production plan status (backport #52713)
2026-02-17 11:40:07 +05:30
Mihir Kandoi
97a6610c0c fix: production plan status
(cherry picked from commit b3e6b304e4)
2026-02-17 05:53:54 +00:00
ili.ad
9ec30319e4 fix(postgres): validate against period closing using MAX(period_end_date) (#51554)
* fix(postgres): validate against period closing using MAX(period_end_date)

* refactor: remove non-existent field

---------

Co-authored-by: Matt Howard <github.severity519@passmail.net>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-02-17 05:02:53 +00:00
rohitwaghchaure
e511503597 Merge pull request #52700 from rohitwaghchaure/fixed-negative-stock-validation-sle-v15
fix: consider sle for negative stock validation
2026-02-16 23:56:18 +05:30
Rohit Waghchaure
ca79f6478a fix: consider sle for negative stock validation 2026-02-16 23:29:59 +05:30
rohitwaghchaure
9ece276e76 Merge pull request #52695 from frappe/mergify/bp/version-15-hotfix/pr-52691
fix: cancel SABB if SLE cancelled from LCV (backport #52691)
2026-02-16 21:52:48 +05:30
mergify[bot]
4fe968961a fix(pos_invoice): add correct depends on condition (backport #52689) (#52693)
* fix(pos_invoice): add correct depends on condition (#52689)

* fix(pos_invoice): add correct depends on condition

* fix: show field in sales order

* refactor: eval condition

(cherry picked from commit 219cf6bc57)

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

* chore: resolve conflict

---------

Co-authored-by: Soham Kulkarni <77533095+sokumon@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-02-16 16:01:23 +00:00
Rohit Waghchaure
f2a77d178d fix: cancel SABB if SLE cancelled from LCV
(cherry picked from commit f23a49a25e)
2026-02-16 15:42:04 +00:00
rohitwaghchaure
dbbba7262b Merge pull request #52683 from frappe/mergify/bp/version-15-hotfix/pr-52681
fix: better validation for negative batch (backport #52681)
2026-02-16 16:03:59 +05:30
Rohit Waghchaure
85d18fa7a4 fix: better validation for negative batch
(cherry picked from commit a8636e4f59)
2026-02-16 09:46:46 +00:00
Mihir Kandoi
11a6c5b394 Merge pull request #52671 from frappe/mergify/bp/version-15-hotfix/pr-52670 2026-02-15 14:30:17 +05:30
Mihir Kandoi
e12871b408 fix: total weight does not update when updating items
(cherry picked from commit 63323a2611)
2026-02-15 08:45:04 +00:00
ruthra kumar
a75c2cb4a0 Merge pull request #52645 from frappe/mergify/bp/version-15-hotfix/pr-52644
refactor: use query builder for profitability analysis (backport #52644)
2026-02-12 14:39:35 +05:30
ruthra kumar
c18ed0862e refactor: use query builder for profitability analysis
(cherry picked from commit 5e34325604)
2026-02-12 08:54:48 +00:00
ruthra kumar
8bddefb18b Merge pull request #52641 from frappe/mergify/bp/version-15-hotfix/pr-52640
refactor: use query builder for sales person commission summary (backport #52640)
2026-02-12 12:52:37 +05:30
ruthra kumar
55448b7437 Merge pull request #50911 from Dharanidharan2813/fix/list-index-bp-50860
fix: Payment Terms auto-fetched in Sales Invoice Without enabling the (automatically_fetch_payment_terms) in Account settings  (backport #50149)
2026-02-12 12:51:08 +05:30
ruthra kumar
ac02af476a refactor: use query builder for sales person commission summary
(cherry picked from commit 7105e3fb69)
2026-02-12 07:07:25 +00:00
ruthra kumar
0a1b532f69 Merge pull request #52637 from frappe/mergify/bp/version-15-hotfix/pr-52619
feat: show formatted currency symbol on ledger preview (backport #52619)
2026-02-12 11:46:37 +05:30
Navin-S-R
383648fb59 feat: show formatted currency symbol on ledger preview
(cherry picked from commit 5c8cb1e7ec)
2026-02-12 05:58:43 +00:00
Dharanidharan S
13239a9dee fix(accounts-controller): handle empty items list 2026-02-12 11:18:44 +05:30
diptanilsaha
2c13b2cc22 test: fixed test_make_sales_invoice_with_terms 2026-02-12 11:18:44 +05:30
Diptanil Saha
4da44e2c3f chore: resolve linter issue 2026-02-12 11:18:44 +05:30
Diptanil Saha
a503460bd5 chore: resolve conflict 2026-02-12 11:18:44 +05:30
dharanidharan2813
78a3701f4c fix: Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled
(cherry picked from commit cf1d892d60)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
2026-02-12 11:18:44 +05:30
ruthra kumar
eedb93b2d6 Merge pull request #52100 from frappe/mergify/bp/version-15-hotfix/pr-51739
fix(accounts): correct base grand total and rounded total mismatch (backport #51739)
2026-02-12 11:13:27 +05:30
Diptanil Saha
ea3042bc74 Merge pull request #52614 from frappe/mergify/bp/version-15-hotfix/pr-51155
fix: allow rename for market segment doctype (backport #51155)
2026-02-11 14:56:34 +05:30
diptanilsaha
b977366dcd chore: resolve conflict 2026-02-11 14:41:01 +05:30
diptanilsaha
0a4198718b fix: allow rename for market segment doctype
(cherry picked from commit f3142c4af6)

# Conflicts:
#	erpnext/crm/doctype/market_segment/market_segment.json
2026-02-11 08:59:24 +00:00
Frappe PR Bot
b340d7c6bb chore(release): Bumped to Version 15.97.0
# [15.97.0](https://github.com/frappe/erpnext/compare/v15.96.1...v15.97.0) (2026-02-11)

### Bug Fixes

* Added a missing option to the currency field (backport [#52528](https://github.com/frappe/erpnext/issues/52528)) ([#52586](https://github.com/frappe/erpnext/issues/52586)) ([a6f5b88](a6f5b88f9b))
* **buying:** add supplier group link filters in field level ([436cb8d](436cb8dbfc))
* email campaign timeout issue (backport [#51994](https://github.com/frappe/erpnext/issues/51994)) ([#52555](https://github.com/frappe/erpnext/issues/52555)) ([6c9681b](6c9681ba4c))
* enabling skip delivery option for order type maintenance ([a8f05ca](a8f05cadea))
* **gross profit report:** translate column Sales Invoice ([4e910d8](4e910d8a69))
* **gross-profit:** handle item group filters ([7cd9de2](7cd9de211f))
* **gross-profit:** handle returns outside sale period ([303dac2](303dac262c))
* handle gross profit and percentage for return invoices ([bde19ab](bde19ab010))
* **manufacturing:** fix chart period keys ([99f3a7e](99f3a7e4cf))
* **manufacturing:** handle None value for actual_end_date ([f965b35](f965b352c8))
* **map_current_doc:** prevent mutation of query args in get_query (backport [#52202](https://github.com/frappe/erpnext/issues/52202)) ([#52583](https://github.com/frappe/erpnext/issues/52583)) ([9519773](9519773c5c))
* merge taxes in purchase receipt when get items from multiple purchase invoices ([#51422](https://github.com/frappe/erpnext/issues/51422)) ([68338ab](68338abe07))
* **quotation:** ignore zero ordered_qty ([ad92c02](ad92c021f7))
* rate comparison in stock reco ([cacca81](cacca812ed))
* remove incorrect validation from email digest throwing spurious error (backport [#51827](https://github.com/frappe/erpnext/issues/51827)) ([#52582](https://github.com/frappe/erpnext/issues/52582)) ([b034f3d](b034f3d3db))
* resolve conflicts ([36e2cf4](36e2cf49f3))
* return None instead of 0 if valuation rate is falsy ([195f020](195f020636))
* stock balance report issue ([bda7220](bda7220b70))
* **stock:** add is group filter for warehouse fields ([5b7ee0a](5b7ee0af66))
* **stock:** ignore pos reserved batches for stock levels ([635a421](635a421807))
* **stock:** inward stock for pick list test record ([5a42ff0](5a42ff0c3c))
* **stock:** set source warehouse for issue type ([19dca36](19dca36dec))
* **stock:** update target field attribute ([9cfd704](9cfd704eef))
* validate asset movement transaction date (backport [#52340](https://github.com/frappe/erpnext/issues/52340)) ([#52560](https://github.com/frappe/erpnext/issues/52560)) ([eea8cb5](eea8cb5885))

### Features

* allow negative stock for the batch item ([4c094c3](4c094c3d86))
2026-02-11 04:58:18 +00:00
ruthra kumar
0bd3c3b566 Merge pull request #52598 from frappe/version-15-hotfix
chore: release v15
2026-02-11 10:26:50 +05:30
Dharanidharan S
8bdbb24d73 fix(accounts): correct base grand total and rounded total mismatch (#51739)
(cherry picked from commit d82c92a237)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2026-02-10 19:05:23 +05:30
ruthra kumar
442e46c80f Merge pull request #52603 from frappe/mergify/bp/version-15-hotfix/pr-52017
fix(gross-profit): handle returns outside the given sale period (backport #52017)
2026-02-10 18:24:27 +05:30
ruthra kumar
83a72b8b30 Merge pull request #52413 from frappe/mergify/bp/version-15-hotfix/pr-51745
fix(gross profit report): translate column Sales Invoice (backport #51745)
2026-02-10 18:23:04 +05:30
ruthra kumar
a50836ab07 Merge pull request #52553 from frappe/mergify/bp/version-15-hotfix/pr-52501
fix(quotation): ignore zero ordered_qty (backport #52501)
2026-02-10 18:22:08 +05:30
Kavin
e9c4762309 Merge pull request #52541 from frappe/mergify/bp/version-15-hotfix/pr-52516
fix(stock): ignore pos reserved batches for stock levels (backport #52516)
2026-02-10 18:21:21 +05:30
Navin-S-R
7cd9de211f fix(gross-profit): handle item group filters
(cherry picked from commit 047b278791)
2026-02-10 12:41:08 +00:00
Navin-S-R
da37fea583 test: fix test assertions to use index-based totals
(cherry picked from commit fdfa7bc963)
2026-02-10 12:41:08 +00:00
Navin-S-R
a912b78bb8 test: validate sales person wise gross profit
(cherry picked from commit 3ab978ab46)
2026-02-10 12:41:08 +00:00
Navin-S-R
8ba5ef683f test: validate return invoice profit and profit percentage
(cherry picked from commit 4da3d43013)
2026-02-10 12:41:08 +00:00
Navin-S-R
bde19ab010 fix: handle gross profit and percentage for return invoices
(cherry picked from commit 51709f032f)
2026-02-10 12:41:07 +00:00
Navin-S-R
303dac262c fix(gross-profit): handle returns outside sale period
(cherry picked from commit 67d8223f73)
2026-02-10 12:41:07 +00:00
Kavin
3e4bd3040a Merge pull request #52591 from aerele/backport-52527
fix(stock): correct warehouse mapping for material issue (backport #52527)
2026-02-10 15:33:53 +05:30
Kavin
ae490804f9 chore: fix failing pre-commit checks
- Remove empty line with spaces
2026-02-10 15:02:23 +05:30
ljain112
e57f3fe727 test: update expected values for tax withholding calculations in tests 2026-02-10 14:40:53 +05:30
ljain112
68099a9b5c fix: set base_tax_withholding_net_total for jv in tds report 2026-02-10 13:53:31 +05:30
Kavin
36e2cf49f3 fix: resolve conflicts
- Remove POS Settings configuration for version 15 backport.
2026-02-10 13:35:52 +05:30
ljain112
ed42d54989 fix: add base_tax_withholding_net_total to tax withholding report 2026-02-10 13:30:12 +05:30
ljain112
b740846b68 refactor: update labels for tax withholding reports columns to improve clarity 2026-02-10 13:08:35 +05:30
Pandiyan37
5a42ff0c3c fix(stock): inward stock for pick list test record 2026-02-10 12:14:54 +05:30
Pandiyan37
37ca45ea49 test(stock): add test to check from warehouse for issue type 2026-02-10 12:11:59 +05:30
Pandiyan37
19dca36dec fix(stock): set source warehouse for issue type 2026-02-10 12:10:34 +05:30
mergify[bot]
a6f5b88f9b fix: Added a missing option to the currency field (backport #52528) (#52586)
fix: Added a missing option to the currency field (#52528)

(cherry picked from commit da07f84e44)

Co-authored-by: El-Shafei H. <el.shafei.developer@gmail.com>
2026-02-09 20:59:58 +00:00
mergify[bot]
6c9681ba4c fix: email campaign timeout issue (backport #51994) (#52555)
fix: email campaign timeout issue (#51994)

* fix: email campaign timeout issue

* refactor: email campaign backend logic

* refactor: use sendmail instead of manually batching

(cherry picked from commit 22123dd955)

Co-authored-by: Pratik Badhe <badhepd@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-02-10 02:10:05 +05:30
mergify[bot]
9519773c5c fix(map_current_doc): prevent mutation of query args in get_query (backport #52202) (#52583)
fix(map_current_doc): prevent mutation of query args in get_query (#52202)

(cherry picked from commit 23a73c9cdb)

Co-authored-by: V Shankar <shankarv292002@gmail.com>
2026-02-10 01:26:16 +05:30
mergify[bot]
eea8cb5885 fix: validate asset movement transaction date (backport #52340) (#52560)
* fix: validate asset movement transaction date (#52340)

* fix: validate asset transaction date

* fix: validate asset transaction date

* fix: add translation in validate_transaction_date

* test: test_movement_transaction_date

* fix: to ensure test reliability

(cherry picked from commit e98b68c38f)

# Conflicts:
#	erpnext/assets/doctype/asset_movement/test_asset_movement.py

* chore: fix conflicts

Removed unused imports and cleaned up code.

---------

Co-authored-by: Poojashree T R <159940572+22-poojashree@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-02-10 01:02:23 +05:30
Trusted Computer
b034f3d3db fix: remove incorrect validation from email digest throwing spurious error (backport #51827) (#52582) 2026-02-09 23:45:56 +05:30
rohitwaghchaure
cf851cfa56 Merge pull request #52557 from frappe/mergify/bp/version-15-hotfix/pr-52550
feat: allow negative stock for the batch item (backport #52550)
2026-02-09 20:07:43 +05:30
rohitwaghchaure
7f69556c45 chore: fix conflicts 2026-02-09 16:31:33 +05:30
Rohit Waghchaure
4c094c3d86 feat: allow negative stock for the batch item
(cherry picked from commit 376ab0e346)

# Conflicts:
#	erpnext/stock/doctype/stock_settings/stock_settings.json
2026-02-09 10:50:24 +00:00
ravibharathi656
ad92c021f7 fix(quotation): ignore zero ordered_qty
(cherry picked from commit 32ea37035e)
2026-02-09 10:38:44 +00:00
Sudharsanan11
aedab5c210 test(stock): add test to ignore pos reserved batches for stock levels
(cherry picked from commit 47ac67f7a2)
2026-02-09 06:28:42 +00:00
Sudharsanan11
635a421807 fix(stock): ignore pos reserved batches for stock levels
(cherry picked from commit 277ba9cb79)
2026-02-09 06:28:42 +00:00
Mihir Kandoi
816cbdea0d Merge pull request #52523 from frappe/mergify/bp/version-15-hotfix/pr-52497
fix: add is_group filter for supplier_group and warehouse fields (backport #52497)
2026-02-07 22:02:24 +05:30
Mihir Kandoi
66a4823640 chore: resolve conflicts 2026-02-07 21:45:35 +05:30
Sudharsanan11
5b7ee0af66 fix(stock): add is group filter for warehouse fields
(cherry picked from commit a9829f5f7b)
2026-02-07 16:11:16 +00:00
Sudharsanan11
436cb8dbfc fix(buying): add supplier group link filters in field level
(cherry picked from commit cfdc554a19)

# Conflicts:
#	erpnext/buying/doctype/supplier/supplier.json
2026-02-07 16:11:16 +00:00
Mihir Kandoi
ad2b8d2455 Merge pull request #52440 from frappe/mergify/bp/version-15-hotfix/pr-52416
fix(stock): update target field attribute (backport #52416)
2026-02-06 12:53:38 +05:30
Mihir Kandoi
2a1a7fd1f6 Merge pull request #52487 from frappe/mergify/bp/version-15-hotfix/pr-52219
fix: enabling skip delivery option for order type maintenance (backport #52219)
2026-02-06 12:46:22 +05:30
Pandiyan37
d0a8639a2d test(stock): testcase for different inventory dimension
(cherry picked from commit 21d0ee8db1)
2026-02-06 12:28:22 +05:30
Pandiyan37
9cfd704eef fix(stock): update target field attribute
(cherry picked from commit 7e08154217)
2026-02-06 12:28:22 +05:30
mergify[bot]
29b35494da Merge pull request #52483 from frappe/mergify/bp/version-15-hotfix/pr-52475
fix: do not show update stock flag unneccessarily (backport #52475)
2026-02-06 06:46:54 +00:00
Mihir Kandoi
292f17b1b0 chore: resolve conflicts 2026-02-06 12:12:59 +05:30
Mihir Kandoi
740dd878e9 chore: resolve conflicts 2026-02-06 12:12:24 +05:30
Nishka Gosalia
a8f05cadea fix: enabling skip delivery option for order type maintenance
(cherry picked from commit 1a22e3cb61)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.json
#	erpnext/selling/doctype/sales_order/test_sales_order.py
2026-02-06 06:39:47 +00:00
Frappe PR Bot
9c2b4f611d chore(release): Bumped to Version 15.96.1
## [15.96.1](https://github.com/frappe/erpnext/compare/v15.96.0...v15.96.1) (2026-02-05)

### Bug Fixes

* stock balance report issue ([3d525ad](3d525addbe))
2026-02-05 10:53:47 +00:00
rohitwaghchaure
350282f0cc Merge pull request #52463 from frappe/mergify/bp/version-15/pr-52462
fix: stock balance report issue (backport #52462)
2026-02-05 16:20:43 +05:30
Rohit Waghchaure
3d525addbe fix: stock balance report issue
(cherry picked from commit bda7220b70)
2026-02-05 10:20:21 +00:00
rohitwaghchaure
3cf10fafdf Merge pull request #52462 from rohitwaghchaure/fixed-stock-reco-balance-value-v15
fix: stock balance report issue
2026-02-05 15:49:33 +05:30
Rohit Waghchaure
bda7220b70 fix: stock balance report issue 2026-02-05 15:00:40 +05:30
ruthra kumar
c62c30f7a3 Merge pull request #52425 from frappe/mergify/bp/version-15-hotfix/pr-51990
refactor: use https over http while saving website link (backport #51990)
2026-02-05 11:13:42 +05:30
ruthra kumar
d91cf01970 refactor: patch partner_website for old data
(cherry picked from commit 8db29b0a81)

# Conflicts:
#	erpnext/patches.txt
2026-02-05 10:58:29 +05:30
Mihir Kandoi
da0776a38c Merge pull request #52429 from frappe/mergify/bp/version-15-hotfix/pr-52427 2026-02-04 20:18:15 +05:30
archielister
9efdcf208a fix for obtaining bom_no
(cherry picked from commit e4df0a393a)
2026-02-04 14:33:05 +00:00
ruthra kumar
fb525fec80 refactor: scrub http and use https in sales partner
(cherry picked from commit 8cf31548f2)
2026-02-04 12:32:22 +00:00
Mihir Kandoi
f31bb6ad4a Merge pull request #52420 from frappe/mergify/bp/version-15-hotfix/pr-51773
fix(manufacturing): refactor production analytics report (backport #51773)
2026-02-04 17:24:13 +05:30
Sudharsanan11
99f3a7e4cf fix(manufacturing): fix chart period keys
(cherry picked from commit 27091e5168)
2026-02-04 11:28:05 +00:00
Sudharsanan11
f965b352c8 fix(manufacturing): handle None value for actual_end_date
(cherry picked from commit 16f09141da)
2026-02-04 11:28:05 +00:00
elshafei-developer
4e910d8a69 fix(gross profit report): translate column Sales Invoice
(cherry picked from commit 3e39d13172)
2026-02-04 09:18:13 +00:00
Mihir Kandoi
d93ba985e4 Merge pull request #52407 from frappe/mergify/bp/version-15-hotfix/pr-52383
fix: rate comparison in stock reco (backport #52383)
2026-02-04 12:32:56 +05:30
Mihir Kandoi
195f020636 fix: return None instead of 0 if valuation rate is falsy
(cherry picked from commit e8d1e9d946)
2026-02-04 06:48:17 +00:00
Mihir Kandoi
cacca812ed fix: rate comparison in stock reco
(cherry picked from commit f1b4fe12a2)
2026-02-04 06:48:16 +00:00
ruthra kumar
4347efdf2c Merge pull request #52387 from frappe/mergify/bp/version-15-hotfix/pr-51422
fix: merge taxes in purchase receipt when get items from multiple purchase invoices (backport #51422)
2026-02-04 09:52:23 +05:30
NaviN
68338abe07 fix: merge taxes in purchase receipt when get items from multiple purchase invoices (#51422)
* fix: merge taxes in purchase receipt when get items from multiple purchase invoices

* fix: make merge tax configurable

* chore: follow standard merge taxes method

* chore: follow standard merge taxes method

(cherry picked from commit 6fde0a6261)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
2026-02-04 07:19:28 +05:30
Frappe PR Bot
6ba8725940 chore(release): Bumped to Version 15.96.0
# [15.96.0](https://github.com/frappe/erpnext/compare/v15.95.2...v15.96.0) (2026-02-03)

### Bug Fixes

* add docstatus condition to get_sales_invoice_item function ([#51517](https://github.com/frappe/erpnext/issues/51517)) ([afc4c85](afc4c856f8))
* add missing param ([a61ad15](a61ad15998))
* add precision to rejected batch no qty calculation ([d5570f8](d5570f83d2))
* backport Switzerland VAT rates update to version-15 ([#52244](https://github.com/frappe/erpnext/issues/52244)) ([f5481dc](f5481dc7d5))
* **bank_account:** `is_company_account` related validations (backport [#51887](https://github.com/frappe/erpnext/issues/51887)) ([#51921](https://github.com/frappe/erpnext/issues/51921)) ([7226066](7226066772))
* **barcode:** failing request when item has both batch and serial ([19e0d75](19e0d75c22))
* correct exchange gain loss in ppr ([e42f8ff](e42f8ffd5d))
* duplicate account number (Indonesia COA) (backport [#52080](https://github.com/frappe/erpnext/issues/52080)) ([#52316](https://github.com/frappe/erpnext/issues/52316)) ([ac1f29d](ac1f29d5cb))
* hide close button on WO if WO is completed ([bd96868](bd96868736))
* imports ([528a482](528a482240))
* include credit notes in project gross margin calculation ([d9d48da](d9d48da505))
* journal auditing voucher print date to use posting_date ([6413ce4](6413ce467f))
* **mode of payment:** use valid syntax (backport [#51542](https://github.com/frappe/erpnext/issues/51542)) ([#52134](https://github.com/frappe/erpnext/issues/52134)) ([22869b6](22869b6f9d))
* negative stock for purchase return ([c30d76a](c30d76ae68))
* populate contact fields when creating quotation from customer ([78f8922](78f8922a9c))
* production plan not considering planning datetime when creating WO ([7b6c7c3](7b6c7c3e27))
* **profit and loss statement:** exclude non period columns ([32c5861](32c5861919))
* remove unneccessary check ([6a68155](6a681557a9))
* reset incoming rate in selling controller if there are changes in item ([c6937c8](c6937c8375))
* revert to old orm ([7e01ae9](7e01ae9e4a))
* **RFQ:** render email templates for preview and sending ([07c5622](07c56221a5))
* **stock:** add stock recon opening stock condition ([0cbb7f8](0cbb7f8714))
* **stock:** fetch batch wise valuation rate in get_items ([f1ba825](f1ba825818))
* **stock:** ignore packing slip while cancelling the sales invoice ([e6083a5](e6083a57de))
* **stock:** include subcontracting order qty while calculating the bin qty ([ba17fdd](ba17fdd072))
* **stock:** remove is_return condition on pos batch qty calculation ([a638dec](a638dece6b))
* **stock:** set incoming_rate with lcv rate for internal purchase ([41c592a](41c592a1a8))
* **subcontracting:** include item bom in supplied items grouping key ([3b12d60](3b12d60877))
* test cases ([2c74491](2c74491eb6))
* validate over ordering of quotation ([0e60750](0e60750bd8))
* validation when more than one FG items in repack stock entry ([fec3a8b](fec3a8b511))
* zero valuation rate if returning from different warehouse ([28929df](28929df0e8))

### Features

* **delivery-note:** add status indicator when document is partially billed ([e5e3b8a](e5e3b8a6ae))
* filter to display trial balance report without group account (backport [#48486](https://github.com/frappe/erpnext/issues/48486)) ([#52146](https://github.com/frappe/erpnext/issues/52146)) ([f48b4cd](f48b4cda50))
2026-02-03 17:18:56 +00:00
rohitwaghchaure
d0f96c48cf Merge pull request #52348 from frappe/version-15-hotfix
chore: release v15
2026-02-03 22:47:29 +05:30
ruthra kumar
12be1dca7d Merge pull request #52378 from frappe/mergify/bp/version-15-hotfix/pr-51651
fix: correct exchange gain loss in ppr (backport #51651)
2026-02-03 20:59:56 +05:30
Mihir Kandoi
2d2bbe5fc2 Merge pull request #52384 from frappe/mergify/bp/version-15-hotfix/pr-52259
fix(stock): include subcontracting order qty while calculating the bin qty (backport #52259)
2026-02-03 20:48:53 +05:30
Mihir Kandoi
a0da47d7f4 Merge pull request #52381 from frappe/mergify/bp/version-15-hotfix/pr-52374
fix(stock): fetch batch wise valuation rate in get_items (backport #52374)
2026-02-03 20:32:15 +05:30
Mihir Kandoi
c0116bcde5 chore: resolve conflicts 2026-02-03 20:30:40 +05:30
Sudharsanan11
ba17fdd072 fix(stock): include subcontracting order qty while calculating the bin qty
(cherry picked from commit de8f8ef9f4)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
2026-02-03 14:55:08 +00:00
kavin-114
f1ba825818 fix(stock): fetch batch wise valuation rate in get_items
(cherry picked from commit c5df570262)
2026-02-03 14:47:36 +00:00
ravibharathi656
e42f8ffd5d fix: correct exchange gain loss in ppr
(cherry picked from commit 02e96039ac)
2026-02-03 14:35:42 +00:00
rohitwaghchaure
c9955ddb35 Merge pull request #52369 from rohitwaghchaure/fixed-zero-incoming-rate-58744
fix: zero valuation rate if returning from different warehouse
2026-02-03 19:10:37 +05:30
Rohit Waghchaure
28929df0e8 fix: zero valuation rate if returning from different warehouse 2026-02-03 18:54:36 +05:30
ruthra kumar
6027c25c48 Merge pull request #52366 from frappe/mergify/bp/version-15-hotfix/pr-52279
fix(profit and loss statement): exclude non period columns (backport #52279)
2026-02-03 17:47:03 +05:30
ravibharathi656
32c5861919 fix(profit and loss statement): exclude non period columns
(cherry picked from commit 6180e5eb53)
2026-02-03 11:59:45 +00:00
ruthra kumar
e7297f2fc0 Merge pull request #52364 from frappe/mergify/bp/version-15-hotfix/pr-52160
fix(stock): remove is_return condition on pos batch qty calculation (backport #52160)
2026-02-03 17:20:04 +05:30
ruthra kumar
d14d09c286 Merge pull request #52361 from frappe/mergify/bp/version-15-hotfix/pr-51997
Add partially billed status indicator (backport #51997)
2026-02-03 17:19:21 +05:30
kavin-114
5b5d0f56de test: add unit test case for pos reserved with return qty
(cherry picked from commit 12ec997027)
2026-02-03 11:11:33 +00:00
kavin-114
a638dece6b fix(stock): remove is_return condition on pos batch qty calculation
(cherry picked from commit 2c19c1fd06)
2026-02-03 11:11:32 +00:00
rohitwaghchaure
d479930bef Merge pull request #52335 from aerele/v15/pr-52281
fix(stock): add stock recon opening stock condition
2026-02-03 16:40:22 +05:30
rohitwaghchaure
edd2814fc4 Merge branch 'version-15' into version-15-hotfix 2026-02-03 16:36:52 +05:30
Dharanidharan2813
e5e3b8a6ae feat(delivery-note): add status indicator when document is partially billed
(cherry picked from commit 7767000ccf)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.json
2026-02-03 16:30:49 +05:30
rohitwaghchaure
a1b1011555 Merge pull request #52343 from rohitwaghchaure/fixed-negative-stock-error-for-purchase-return-v15
fix: negative stock for purchase return
2026-02-03 16:16:08 +05:30
Rohit Waghchaure
c30d76ae68 fix: negative stock for purchase return 2026-02-03 15:56:53 +05:30
ruthra kumar
2c9c5dcc85 Merge pull request #52336 from frappe/mergify/bp/version-15-hotfix/pr-52280
fix(stock): ignore packing slip while cancelling the sales invoice (backport #52280)
2026-02-03 13:59:09 +05:30
Sudharsanan11
e6083a57de fix(stock): ignore packing slip while cancelling the sales invoice
(cherry picked from commit c58887b44a)
2026-02-03 08:24:45 +00:00
kavin-114
0cbb7f8714 fix(stock): add stock recon opening stock condition 2026-02-03 13:41:21 +05:30
ruthra kumar
fa01eefd89 Merge pull request #52329 from frappe/mergify/bp/version-15-hotfix/pr-51655
fix: include credit notes in project gross margin calculation (backport #51655)
2026-02-03 12:19:41 +05:30
ravibharathi656
d9d48da505 fix: include credit notes in project gross margin calculation
(cherry picked from commit a378fee8e0)
2026-02-03 06:07:34 +00:00
Mihir Kandoi
e1c71e0b8f Merge pull request #52310 from frappe/mergify/bp/version-15-hotfix/pr-52246
fix: validation considers moving average by default instead of set va… (backport #52246)
2026-02-03 09:44:50 +05:30
Mihir Kandoi
a61ad15998 fix: add missing param 2026-02-03 09:29:46 +05:30
Mihir Kandoi
c114f8445b Merge pull request #52323 from frappe/mergify/bp/version-15-hotfix/pr-52184
fix(subcontracting): include item bom in supplied items grouping key (backport #52184)
2026-02-03 09:27:55 +05:30
Mihir Kandoi
c6127575f5 chore: resolve conflicts 2026-02-03 09:12:14 +05:30
Mihir Kandoi
12a2e98751 chore: resolve conflicts 2026-02-03 09:10:41 +05:30
Sudharsanan11
1d7ba16caf test(subcontracting): add test for consumed_qty calculation with similar finished goods
(cherry picked from commit 4d9412181c)
2026-02-03 03:39:49 +00:00
Sudharsanan11
3b12d60877 fix(subcontracting): include item bom in supplied items grouping key
(cherry picked from commit 0d372a62a1)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py
2026-02-03 03:39:49 +00:00
mergify[bot]
ac1f29d5cb fix: duplicate account number (Indonesia COA) (backport #52080) (#52316)
Co-authored-by: Apriliansyah Idris <apriliansyahidris@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix: duplicate account number (Indonesia COA) (#52080)
2026-02-02 19:37:52 +00:00
Solede
f5481dc7d5 fix: backport Switzerland VAT rates update to version-15 (#52244)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 00:58:42 +05:30
Mihir Kandoi
5f60c0e85e Merge pull request #52246 from mihir-kandoi/st58765
(cherry picked from commit 135a433018)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2026-02-02 15:15:17 +00:00
Mihir Kandoi
7b059a9221 Merge pull request #52307 from frappe/mergify/bp/version-15-hotfix/pr-52304
fix: populate contact fields when creating quotation from customer (backport #52304)
2026-02-02 20:32:16 +05:30
Mihir Kandoi
78f8922a9c fix: populate contact fields when creating quotation from customer
(cherry picked from commit 75b2c2c83d)
2026-02-02 14:46:21 +00:00
Mihir Kandoi
09c56abeb0 Merge pull request #52301 from frappe/mergify/bp/version-15-hotfix/pr-52286
fix: reset incoming rate in selling controller if there are changes i… (backport #52286)
2026-02-02 20:03:13 +05:30
Mihir Kandoi
c6937c8375 fix: reset incoming rate in selling controller if there are changes in item
(cherry picked from commit 2d6b43fd54)
2026-02-02 14:17:43 +00:00
rohitwaghchaure
99ad34d686 Merge pull request #52239 from frappe/mergify/bp/version-15-hotfix/pr-52232
fix: validation when more than one FG items in repack stock entry (backport #52232)
2026-02-02 14:10:48 +05:30
ruthra kumar
09ab2653d1 Merge pull request #52283 from frappe/mergify/bp/version-15-hotfix/pr-52200
fix(accounts): correct date in Journal Auditing Voucher print format (backport #52200)
2026-02-02 12:54:47 +05:30
Tamal Majumdar
6413ce467f fix: journal auditing voucher print date to use posting_date
(cherry picked from commit 43e2495df8)
2026-02-02 07:21:21 +00:00
Mihir Kandoi
a5156f696c Merge pull request #52275 from frappe/mergify/bp/version-15-hotfix/pr-52274 2026-02-02 11:06:19 +05:30
Mihir Kandoi
528a482240 fix: imports 2026-02-02 10:51:27 +05:30
Mihir Kandoi
d9d4b9b117 test: over ordering of quotation items
(cherry picked from commit 53e58f6678)
2026-02-02 10:51:27 +05:30
Mihir Kandoi
a4ad4e8279 Merge pull request #52277 from frappe/mihir-kandoi-patch-1 2026-02-02 10:49:30 +05:30
Mihir Kandoi
d516110572 chore: fix py error on v15 2026-02-02 10:34:23 +05:30
Rohit Waghchaure
6d14cb0c6b chore: fix conflicts 2026-02-01 16:30:59 +05:30
Mihir Kandoi
d30dacce80 Merge pull request #52229 from frappe/mergify/bp/version-15-hotfix/pr-52222 2026-02-01 13:46:46 +05:30
Mihir Kandoi
7e01ae9e4a fix: revert to old orm 2026-02-01 10:26:14 +05:30
Mihir Kandoi
42f94f1aba chore: remove incorrect import 2026-01-31 20:36:46 +05:30
Mihir Kandoi
6a681557a9 fix: remove unneccessary check 2026-01-31 20:27:02 +05:30
Mihir Kandoi
7ab59aa094 chore: resolve conflicts 2026-01-31 20:25:00 +05:30
Mihir Kandoi
c3b92075f0 chore: resolve conflicts
Removed old patch entries and updated the list.
2026-01-31 20:20:36 +05:30
Mihir Kandoi
4017936bef chore: resolve conflicts 2026-01-31 20:19:07 +05:30
Mihir Kandoi
6cecae288c chore: resolve conflicts 2026-01-31 20:18:22 +05:30
Rohit Waghchaure
fec3a8b511 fix: validation when more than one FG items in repack stock entry
(cherry picked from commit 6423ce2fa7)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2026-01-31 07:16:29 +00:00
Mihir Kandoi
2c74491eb6 fix: test cases
(cherry picked from commit 36f1e3572c)
2026-01-30 14:00:29 +00:00
Mihir Kandoi
0e60750bd8 fix: validate over ordering of quotation
(cherry picked from commit 4cc306d2d8)

# Conflicts:
#	erpnext/controllers/status_updater.py
#	erpnext/patches.txt
#	erpnext/selling/doctype/quotation/quotation.py
#	erpnext/selling/doctype/quotation_item/quotation_item.json
2026-01-30 14:00:28 +00:00
Mihir Kandoi
4dfc5671b0 Merge pull request #52217 from frappe/mergify/bp/version-15-hotfix/pr-52209
fix: add precision to rejected batch no qty calculation (backport #52209)
2026-01-30 12:21:00 +05:30
Mihir Kandoi
003eb02e24 Merge pull request #52214 from frappe/mergify/bp/version-15-hotfix/pr-52213
fix: hide close button on WO if WO is completed (backport #52213)
2026-01-30 12:06:47 +05:30
Mihir Kandoi
d5570f83d2 fix: add precision to rejected batch no qty calculation
(cherry picked from commit 838d245215)
2026-01-30 06:35:43 +00:00
Mihir Kandoi
bd96868736 fix: hide close button on WO if WO is completed
(cherry picked from commit 6e17ccf499)
2026-01-30 06:29:12 +00:00
Mihir Kandoi
85f7196eb5 Merge pull request #52211 from frappe/mergify/bp/version-15-hotfix/pr-52210
fix(barcode): failing request when item has both batch and serial (backport #52210)
2026-01-30 11:52:42 +05:30
Mihir Kandoi
19e0d75c22 fix(barcode): failing request when item has both batch and serial
(cherry picked from commit 89f6f0f46f)
2026-01-30 06:17:25 +00:00
rohitwaghchaure
3570ab8868 Merge pull request #52191 from frappe/mergify/bp/version-15-hotfix/pr-52158
Add Landed Cost Voucher Amount in Internal Purchase Receipt (backport #52158)
2026-01-29 17:56:11 +05:30
kavin-114
3ccd1b4a6c test: add unit test to check internal purchase with lcv
(cherry picked from commit dd4fd89ef8)
2026-01-29 12:02:34 +00:00
kavin-114
41c592a1a8 fix(stock): set incoming_rate with lcv rate for internal purchase
(cherry picked from commit f0dccc3cd7)
2026-01-29 12:02:34 +00:00
mergify[bot]
ad8c8cb0e8 Merge pull request #52140 from frappe/mergify/bp/version-15-hotfix/pr-52007
Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007)
2026-01-29 14:30:22 +05:30
Mihir Kandoi
7417432ddb Merge pull request #52167 from frappe/mergify/bp/version-15-hotfix/pr-52166
fix: production plan not considering planning datetime when creating WO (backport #52166)
2026-01-29 11:05:39 +05:30
Mihir Kandoi
7b6c7c3e27 fix: production plan not considering planning datetime when creating WO
(cherry picked from commit 4e19c7e8bd)
2026-01-29 05:20:41 +00:00
Aarol D'Souza
df857f8177 Merge pull request #52163 from frappe/mergify/bp/version-15-hotfix/pr-52092
fix(RFQ): render email templates for preview and sending (backport #52092)
2026-01-29 09:22:32 +05:30
AarDG10
43fc1ae4bf ci: minor text correction
(cherry picked from commit 37cdae2f34)
2026-01-29 03:37:44 +00:00
AarDG10
07c56221a5 fix(RFQ): render email templates for preview and sending
(cherry picked from commit 525b3960e1)
2026-01-29 03:37:44 +00:00
Navin-S-R
273029d0f0 fix: recalculate tax withholding during Purchase Order child update 2026-01-28 18:51:49 +05:30
mergify[bot]
f48b4cda50 feat: filter to display trial balance report without group account (backport #48486) (#52146)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-01-28 11:40:18 +00:00
mergify[bot]
7226066772 fix(bank_account): is_company_account related validations (backport #51887) (#51921)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-01-28 10:22:09 +00:00
aymenit2008
afc4c856f8 fix: add docstatus condition to get_sales_invoice_item function (#51517) 2026-01-28 15:43:33 +05:30
mergify[bot]
22869b6f9d fix(mode of payment): use valid syntax (backport #51542) (#52134)
Co-authored-by: ervishnucs <ervishnucs369@gmail.com>
2026-01-28 15:10:31 +05:30
177 changed files with 4589 additions and 1354 deletions

View File

@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext Verion -
ERPNext version -
validations:
required: true

View File

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

View File

@@ -33,6 +33,17 @@
},
"account_number": "1151.000"
},
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1152.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.002",
"account_type": "Tax"
},
"account_number": "1152.000"
},
"account_number": "1150.000"
},
"Kas": {

View File

@@ -48,6 +48,7 @@
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "currency",
"read_only": 1
},
{

View File

@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
});
}
},
is_company_account: function (frm) {
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
},
});

View File

@@ -52,6 +52,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company Account",
"mandatory_depends_on": "is_company_account",
"options": "Account"
},
{
@@ -98,6 +99,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"mandatory_depends_on": "is_company_account",
"options": "Company"
},
{
@@ -252,7 +254,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2025-08-29 12:32:01.081687",
"modified": "2026-01-20 00:46:16.633364",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",

View File

@@ -52,31 +52,35 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name)
def validate(self):
self.validate_company()
self.validate_account()
self.validate_is_company_account()
self.update_default_bank_account()
def validate_account(self):
if self.account:
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def validate_is_company_account(self):
if self.is_company_account:
if not self.company:
frappe.throw(_("Company is mandatory for company account"))
def validate_company(self):
if self.is_company_account and not self.company:
frappe.throw(_("Company is manadatory for company account"))
if not self.account:
frappe.throw(_("Company Account is mandatory"))
self.validate_account()
@deprecated
def validate_iban(self):
"""Kept for backward compatibility, will be removed in v16."""
validate_iban(self.iban, throw=True)
def validate_account(self):
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def update_default_bank_account(self):
if self.is_default and not self.disabled:
frappe.db.set_value(

View File

@@ -136,6 +136,8 @@ class BankTransaction(Document):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry)
@@ -370,11 +372,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"),
as_dict=True,
)
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt.bank_account != gl_bank_account:
if bt_bank_account != gl_bank_account:
frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt.bank_account, payment_entry.payment_entry, gl_bank_account
bt_bank_account, payment_entry.payment_entry, gl_bank_account
)
)

View File

@@ -4,7 +4,7 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe import _, cint
from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
@@ -33,23 +33,11 @@ class FiscalYear(Document):
self.validate_dates()
self.validate_overlap()
if not self.is_new():
year_start_end_dates = frappe.db.sql(
"""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
def on_update(self):
frappe.cache().delete_key("fiscal_years")
if year_start_end_dates:
if (
getdate(self.year_start_date) != year_start_end_dates[0][0]
or getdate(self.year_end_date) != year_start_end_dates[0][1]
):
frappe.throw(
_(
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
)
)
def on_trash(self):
frappe.cache().delete_key("fiscal_years")
def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date")
@@ -66,28 +54,20 @@ class FiscalYear(Document):
frappe.exceptions.InvalidDates,
)
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
def on_trash(self):
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):
existing_fiscal_years = frappe.db.sql(
"""select name from `tabFiscal Year`
where (
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date)
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
) and name!=%(name)s""",
{
"year_start_date": self.year_start_date,
"year_end_date": self.year_end_date,
"name": self.name or "No Name",
},
as_dict=True,
fy = frappe.qb.DocType("Fiscal Year")
name = self.name or self.year
existing_fiscal_years = (
frappe.qb.from_(fy)
.select(fy.name)
.where(
(fy.year_start_date <= self.year_end_date)
& (fy.year_end_date >= self.year_start_date)
& (fy.name != name)
)
.run(as_dict=True)
)
if existing_fiscal_years:
@@ -110,37 +90,30 @@ class FiscalYear(Document):
frappe.throw(
_(
"Year start date or end date is overlapping with {0}. To avoid please set company"
).format(existing.name),
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
frappe.NameError,
)
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
year_start_end_dates = frappe.db.sql(
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
(doc.name),
)
for fiscal_year, ysd, yed in year_start_end_dates:
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
not frappe.flags.in_test
):
frappe.throw(
_(
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
).format(fiscal_year)
)
@frappe.whitelist()
def auto_create_fiscal_year():
for d in frappe.db.sql(
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
):
fy = frappe.qb.DocType("Fiscal Year")
# Skipped auto-creating Short Year, as it has very rare use case.
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
follow_up_date = add_days(getdate(), days=3)
fiscal_year = (
frappe.qb.from_(fy)
.select(fy.name)
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
.run()
)
for d in fiscal_year:
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
new_fy = frappe.new_doc("Fiscal Year")
new_fy.disabled = cint(current_fy.disabled)
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
@@ -148,6 +121,10 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year)
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
for row in current_fy.companies:
new_fy.append("companies", {"company": row.company})
new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True)

View File

@@ -15,13 +15,14 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company"
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-28 18:01:53.495929",
"modified": "2026-02-20 23:02:26.193606",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year Company",
@@ -30,4 +31,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link | None
company: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -185,7 +185,7 @@
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
"search_index": 1
},
{
@@ -198,7 +198,7 @@
"search_index": 1
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
"fieldname": "reference_due_date",
"fieldtype": "Date",
"label": "Reference Due Date",
@@ -295,7 +295,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-27 12:23:33.157655",
"modified": "2026-02-19 17:01:22.642454",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
"Fees",
"Full and Final Statement",
"Payment Entry",
"Bank Transaction",
]
user_remark: DF.SmallText | None
# end: auto-generated types

View File

@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
let d = locals[cdt][cdn];
return {
filters: [
["Account", "account_type", "in", "Bank, Cash, Receivable"],
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
["Account", "is_group", "=", 0],
["Account", "company", "=", d.company],
],

View File

@@ -516,12 +516,16 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("contact_email", "");
frm.set_value("contact_person", "");
}
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if (!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party"));
frm.set_value("party", "");
return;
}
erpnext.utils.get_employee_contact_details(frm);
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
@@ -1465,16 +1469,15 @@ frappe.ui.form.on("Payment Entry", {
callback: function (r) {
if (!r.exc && r.message) {
// set taxes table
if (r.message) {
for (let tax of r.message) {
if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount";
}
frm.add_child("taxes", tax);
let taxes = r.message;
taxes.forEach((tax) => {
if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount";
}
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
}
});
frm.set_value("taxes", taxes);
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
}
},
});

View File

@@ -1092,20 +1092,32 @@ class PaymentEntry(AccountsController):
self.base_paid_amount + deductions_to_consider
):
self.unallocated_amount = (
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.source_exchange_rate
flt(
(
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.source_exchange_rate
)
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
self.base_received_amount - deductions_to_consider
):
self.unallocated_amount = (
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.target_exchange_rate
flt(
(
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.target_exchange_rate
)
def set_exchange_gain_loss(self):
exchange_gain_loss = flt(

View File

@@ -536,7 +536,7 @@ class PaymentRequest(Document):
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def make_payment_request(**args):
"""Make payment request"""
@@ -548,6 +548,9 @@ def make_payment_request(**args):
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
frappe.has_permission("Payment Request", "create", throw=True)
frappe.has_permission(args.dt, "read", args.dn, throw=True)
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
@@ -822,7 +825,7 @@ def get_print_format_list(ref_doctype):
return {"print_format": print_format_list}
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email()

View File

@@ -60,7 +60,6 @@
"sec_warehouse",
"set_warehouse",
"items_section",
"update_stock",
"scan_barcode",
"last_scanned_warehouse",
"items",
@@ -574,7 +573,6 @@
"label": "Warehouse"
},
{
"depends_on": "update_stock",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
@@ -588,15 +586,6 @@
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
{
"default": "0",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
"oldfieldname": "update_stock",
"oldfieldtype": "Check",
"print_hide": 1
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
@@ -1582,7 +1571,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 22:22:31.471752",
"modified": "2026-02-22 04:18:50.691218",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1627,6 +1616,7 @@
"role": "All"
}
],
"row_format": "Dynamic",
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1,
"sort_field": "modified",
@@ -1635,4 +1625,4 @@
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -183,7 +183,6 @@ class POSInvoice(SalesInvoice):
total_taxes_and_charges: DF.Currency
update_billed_amount_in_delivery_note: DF.Check
update_billed_amount_in_sales_order: DF.Check
update_stock: DF.Check
write_off_account: DF.Link | None
write_off_amount: DF.Currency
write_off_cost_center: DF.Link | None
@@ -652,7 +651,6 @@ class POSInvoice(SalesInvoice):
"tax_category",
"ignore_pricing_rule",
"company_address",
"update_stock",
):
if not for_validate:
self.set(fieldname, profile.get(fieldname))

View File

@@ -838,6 +838,53 @@ class TestPOSInvoice(unittest.TestCase):
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 5)
def test_pos_batch_reservation_with_return_qty(self):
"""
Test POS Invoice reserved qty for batch without bundle with return invoices.
"""
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
se = make_stock_entry(
target="_Test Warehouse - _TC",
item_code="_Batch Item Reserve Return",
qty=30,
basic_rate=100,
)
se.reload()
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
# POS Invoice for the batch without bundle
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "amount": 4500},
)
pos_inv.items[0].batch_no = batch_no
pos_inv.save()
pos_inv.submit()
# POS Invoice return
pos_return = make_sales_return(pos_inv.name)
pos_return.insert()
pos_return.submit()
batches = get_auto_batch_nos(
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
)
for batch in batches:
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 30)
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError,
@@ -1054,7 +1101,6 @@ def create_pos_invoice(**args):
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name

View File

@@ -146,6 +146,7 @@ class POSInvoiceMergeLog(Document):
sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.update_stock = 1
if not sales_invoice.posting_date:
sales_invoice.posting_date = getdate(self.posting_date)
@@ -174,6 +175,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.update_stock = 1
credit_note.posting_date = getdate(self.posting_date)
credit_note.posting_time = get_time(self.posting_time)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
@@ -697,6 +699,7 @@ def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
& (SalesInvoice.docstatus == 1)
)
)

View File

@@ -25,7 +25,6 @@
"validate_stock_on_save",
"print_receipt_on_order_complete",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
@@ -297,7 +296,6 @@
"options": "Print Format"
},
{
"depends_on": "update_stock",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
@@ -312,14 +310,6 @@
"fieldtype": "Check",
"label": "Ignore Pricing Rule"
},
{
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"hidden": 1,
"label": "Update Stock",
"read_only": 1
},
{
"default": "0",
"fieldname": "hide_unavailable_items",
@@ -432,7 +422,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2025-04-14 15:58:20.497426",
"modified": "2026-02-22 04:17:03.308876",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -61,7 +61,6 @@ class POSProfile(Document):
tax_category: DF.Link | None
taxes_and_charges: DF.Link | None
tc_name: DF.Link | None
update_stock: DF.Check
validate_stock_on_save: DF.Check
warehouse: DF.Link
write_off_account: DF.Link

View File

@@ -121,7 +121,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Apply On",
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
"options": "Item Code\nItem Group\nBrand\nTransaction",
"reqd": 1
},
{
@@ -657,7 +657,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-08-20 11:40:07.096854",
"modified": "2026-02-17 12:24:07.553505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
@@ -719,4 +719,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -45,7 +45,7 @@ class PricingRule(Document):
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
apply_discount_on_rate: DF.Check
apply_multiple_pricing_rules: DF.Check
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
apply_recursion_over: DF.Float
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
brands: DF.Table[PricingRuleBrand]
@@ -346,8 +346,7 @@ def apply_pricing_rule(args, doc=None):
args = frappe._dict(args)
if not args.transaction_type:
set_transaction_type(args)
set_transaction_type(args)
# list of dictionaries
out = []
@@ -683,23 +682,23 @@ def remove_pricing_rules(item_list):
return out
def set_transaction_type(args):
if args.transaction_type:
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
if pricing_ctx.transaction_type in ["buying", "selling"]:
return
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
args.transaction_type = "selling"
elif args.doctype in (
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
pricing_ctx.transaction_type = "selling"
elif pricing_ctx.doctype in (
"Material Request",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
):
args.transaction_type = "buying"
elif args.customer:
args.transaction_type = "selling"
pricing_ctx.transaction_type = "buying"
elif pricing_ctx.customer:
pricing_ctx.transaction_type = "selling"
else:
args.transaction_type = "buying"
pricing_ctx.transaction_type = "buying"
@frappe.whitelist()

View File

@@ -20,7 +20,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Brand'",
"fieldname": "brand",
"fieldtype": "Link",
"hidden": 0,
@@ -91,7 +91,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-03-24 14:48:59.649168",
"modified": "2026-02-17 12:17:13.073587",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Brand",
@@ -107,4 +107,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View File

@@ -20,7 +20,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Item Group'",
"fieldname": "item_group",
"fieldtype": "Link",
"hidden": 0,
@@ -91,7 +91,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-03-24 14:48:59.649168",
"modified": "2026-02-17 12:16:57.778471",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Item Group",
@@ -107,4 +107,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View File

@@ -412,8 +412,9 @@ def reconcile(doc: None | str = None) -> None:
for x in allocations:
pr.append("allocation", x)
skip_ref_details_update_for_pe = check_multi_currency(pr)
# reconcile
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
# If Payment Entry, update details only for newly linked references
# This is for performance
@@ -503,6 +504,37 @@ def reconcile(doc: None | str = None) -> None:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
def check_multi_currency(pr_doc):
GL = frappe.qb.DocType("GL Entry")
Account = frappe.qb.DocType("Account")
def get_account_currency(voucher_type, voucher_no):
currency = (
frappe.qb.from_(GL)
.join(Account)
.on(GL.account == Account.name)
.select(Account.account_currency)
.where(
(GL.voucher_type == voucher_type)
& (GL.voucher_no == voucher_no)
& (Account.account_type.isin(["Payable", "Receivable"]))
)
.limit(1)
).run(as_dict=True)
return currency[0].account_currency if currency else None
for allocation in pr_doc.allocation:
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
if reference_currency != invoice_currency:
return True
return False
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None

View File

@@ -603,6 +603,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
@@ -1659,7 +1660,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 19:19:11.380664",
"modified": "2026-02-05 20:45:16.964500",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -38,7 +38,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
@@ -728,9 +728,10 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if item.purchase_receipt:
frappe.throw(
_("Stock cannot be updated against Purchase Receipt {0}").format(
item.purchase_receipt
)
_(
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
).format(self.name, item.purchase_receipt),
title=_("Stock Update Not Allowed"),
)
def validate_for_repost(self):
@@ -1729,10 +1730,6 @@ class PurchaseInvoice(BuyingController):
project_doc.db_update()
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
if self.bill_no:
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
@@ -2083,6 +2080,19 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
if isinstance(args, str):
args = json.loads(args)
def post_parent_process(source_parent, target_parent):
remove_items_with_zero_qty(target_parent)
set_missing_values(source_parent, target_parent)
def remove_items_with_zero_qty(target_parent):
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
def set_missing_values(source_parent, target_parent):
target_parent.run_method("set_missing_values")
if args and args.get("merge_taxes"):
merge_taxes(source_parent, target_parent)
target_parent.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
@@ -2122,7 +2132,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"reset_value": not (args and args.get("merge_taxes")),
"ignore": args.get("merge_taxes") if args else 0,
},
},
target_doc,
)

View File

@@ -43,6 +43,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
"Unreconcile Payment Entries",
"Serial and Batch Bundle",
"Bank Transaction",
"Packing Slip",
];
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {

View File

@@ -701,6 +701,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
"fieldname": "update_stock",
"fieldtype": "Check",
"hide_days": 1,
@@ -2199,7 +2200,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-09-09 14:48:59.472826",
"modified": "2026-02-05 20:43:44.732805",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -854,9 +854,6 @@ class SalesInvoice(SellingController):
if selling_price_list:
self.set("selling_price_list", selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
@@ -1097,7 +1094,9 @@ class SalesInvoice(SellingController):
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
def update_packing_list(self):
if cint(self.update_stock) == 1:
if self.doctype == "POS Invoice" or (
self.doctype == "Sales Invoice" and cint(self.update_stock) == 1
):
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
@@ -1200,6 +1199,9 @@ class SalesInvoice(SellingController):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def process_asset_depreciation(self):
if self.is_internal_transfer():
return
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:

View File

@@ -840,6 +840,7 @@
"fieldtype": "Currency",
"label": "Incoming Rate (Costing)",
"no_copy": 1,
"non_negative": 1,
"options": "Company:company:default_currency",
"print_hide": 1
},
@@ -983,7 +984,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 16:33:55.503777",
"modified": "2026-02-23 14:37:14.853941",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -993,4 +994,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -26,7 +26,7 @@
},
{
"default": "0",
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -85,7 +85,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-01-23 16:20:06.436979",
"modified": "2026-02-16 20:46:34.592604",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
@@ -95,4 +95,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -804,12 +804,19 @@ def validate_against_pcv(is_opening, posting_date, company):
title=_("Invalid Opening Entry"),
)
last_pcv_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
)
# Local import so you don't have to touch file-level imports
from frappe.query_builder.functions import Max
pcv = frappe.qb.DocType("Period Closing Voucher")
last_pcv_date = (
frappe.qb.from_(pcv)
.select(Max(pcv.period_end_date))
.where((pcv.docstatus == 1) & (pcv.company == company))
).run(pluck=True)[0]
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date))
message = _("Books have been closed till the period ending on {0}.").format(formatdate(last_pcv_date))
message += "</br >"
message += _("You cannot create/amend any accounting entries till this date.")
frappe.throw(message, title=_("Period Closed"))

View File

@@ -0,0 +1,43 @@
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
<p>{{ _("Fiscal Year Details") }}</p>
<table style="margin-bottom: 1rem; width: 70%">
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
<td>{{ doc.name }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
</tr>
{% if doc.companies|length > 0 %}
<tr>
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
{% if doc.companies|length < 2 %}
{{ _("Company") }}
{% else %}
{{ _("Companies") }}
{% endif %}
</td>
<td>{{ doc.companies[0].company }}</td>
</tr>
{% for idx in range(1, doc.companies|length) %}
<tr>
<td>{{ doc.companies[idx].company }}</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% if doc.disabled %}
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
{% endif %}
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>

View File

@@ -1,7 +1,7 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.auto_created",
"condition": "doc.auto_created == 1",
"creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0,
"docstatus": 0,
@@ -11,19 +11,22 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "<h3>{{_(\"Fiscal Year\")}}</h3>\n\n<p>{{ _(\"New fiscal year created :- \") }} {{ doc.name }}</p>",
"modified": "2018-04-25 14:30:38.588534",
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
"message_type": "HTML",
"modified": "2026-02-21 15:59:07.775679",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Notification for new fiscal year",
"owner": "Administrator",
"recipients": [
{
"email_by_role": "Accounts User"
"receiver_by_role": "Accounts Manager"
},
{
"email_by_role": "Accounts Manager"
"receiver_by_role": "Accounts User"
}
],
"subject": "Notification for new fiscal year {{ doc.name }}"
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "{{ _(\"New Fiscal Year {0} - Review Required\").format(doc.name) }}"
}

View File

@@ -7,18 +7,16 @@ from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Count, Date, Sum
from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
add_years,
cint,
cstr,
date_diff,
flt,
formatdate,
get_last_day,
get_timestamp,
getdate,
nowdate,
)
@@ -302,19 +300,9 @@ def complete_contact_details(party_details):
contact_details = frappe._dict()
if party_details.party_type == "Employee":
contact_details = frappe.db.get_value(
"Employee",
party_details.party,
[
"employee_name as contact_display",
"prefered_email as contact_email",
"cell_number as contact_mobile",
"designation as contact_designation",
"department as contact_department",
],
as_dict=True,
)
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
contact_details = get_employee_contact(party_details.party)
contact_details.update({"contact_person": None, "contact_phone": None})
elif party_details.contact_person:
contact_details = frappe.db.get_value(

View File

@@ -17,7 +17,7 @@
</div>
<div class="col-xs-6">
<table>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
</table>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from frappe.utils import add_days, today
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
if not do_not_submit:
pi = pi.submit()
return pi
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"based_on_payment_terms": 1,
"payment_terms_template": template.name,
"ageing_based_on": "Posting Date",
}
pi = self.create_purchase_invoice(do_not_submit=True)
pi.payment_terms_template = template.name
schedule = get_payment_terms(template.name)
pi.set("payment_schedule", [])
for row in schedule:
row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0))
pi.append("payment_schedule", row)
pi.save()
pi.submit()
report = execute(filters)
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])

View File

@@ -1029,9 +1029,8 @@ class ReceivablePayableReport:
self,
):
self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"):
groups = get_customer_group_with_children(self.filters.customer_group)
groups = get_party_group_with_children("Customer", self.filters.customer_group)
customers = (
qb.from_(self.customer)
.select(self.customer.name)
@@ -1043,14 +1042,18 @@ class ReceivablePayableReport:
self.get_hierarchical_filters("Territory", "territory")
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
customer_ptt = self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
si_ptt = self.add_payment_term_template_filters("Sales Invoice")
sales_ptt = self.ple.against_voucher_no.isin(si_ptt)
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
if self.filters.get("sales_partner"):
self.qb_selection_filter.append(
self.ple.party.isin(
@@ -1075,14 +1078,53 @@ class ReceivablePayableReport:
)
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("supplier_group"))
)
supplier_ptt = self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("payment_terms_template"))
)
pi_ptt = self.add_payment_term_template_filters("Purchase Invoice")
purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt)
self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt]))
def add_payment_term_template_filters(self, dtype):
voucher_type = qb.DocType(dtype)
ptt = (
qb.from_(voucher_type)
.select(voucher_type.name)
.where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template"))
.where(voucher_type.company == self.filters.company)
)
if dtype == "Purchase Invoice":
party = "Supplier"
party_group_type = "supplier_group"
acc_type = "credit_to"
else:
party = "Customer"
party_group_type = "customer_group"
acc_type = "debit_to"
if self.filters.get(party_group_type):
party_groups = get_party_group_with_children(party, self.filters.get(party_group_type))
ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups))
if self.filters.party:
ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party))
if self.filters.cost_center:
cost_centers = get_cost_centers_with_children(self.filters.cost_center)
ptt = ptt.where(voucher_type.cost_center.isin(cost_centers))
if self.filters.party_account:
ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account)
return ptt
def get_hierarchical_filters(self, doctype, key):
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
@@ -1320,20 +1362,26 @@ class ReceivablePayableReport:
self.err_journals = [x[0] for x in results] if results else []
def get_customer_group_with_children(customer_groups):
if not isinstance(customer_groups, list):
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
def get_party_group_with_children(party, party_groups):
if party not in ("Customer", "Supplier"):
return []
all_customer_groups = []
for d in customer_groups:
if frappe.db.exists("Customer Group", d):
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_customer_groups += [c.name for c in children]
group_dtype = f"{party} Group"
if not isinstance(party_groups, list):
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
all_party_groups = []
for d in party_groups:
if frappe.db.exists(group_dtype, d):
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
children = frappe.get_all(
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
)
all_party_groups += children
else:
frappe.throw(_("Customer Group: {0} does not exist").format(d))
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_customer_groups))
return list(set(all_party_groups))
class InitSQLProceduresForAR:

View File

@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"based_on_payment_terms": 1,
"payment_terms_template": template.name,
"ageing_based_on": "Posting Date",
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.payment_terms_template = template.name
schedule = get_payment_terms(template.name)
si.set("payment_schedule", [])
for row in schedule:
row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
si.append("payment_schedule", row)
si.save()
si.submit()
report = execute(filters)
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])

View File

@@ -96,7 +96,7 @@ def execute(filters=None):
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
)
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
@@ -225,18 +225,19 @@ def get_report_summary(
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[2:]]
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
labels = [col.get("label") for col in chart_columns]
asset_data, liability_data, equity_data = [], [], []
for p in columns[2:]:
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if asset:
asset_data.append(asset[-2].get(p.get("fieldname")))
asset_data.append(asset[-2].get(key))
if liability:
liability_data.append(liability[-2].get(p.get("fieldname")))
liability_data.append(liability[-2].get(key))
if equity:
equity_data.append(equity[-2].get(p.get("fieldname")))
equity_data.append(equity[-2].get(key))
datasets = []
if asset_data:

View File

@@ -139,7 +139,7 @@ def execute(filters=None):
True,
)
chart = get_chart_data(columns, data, company_currency)
chart = get_chart_data(period_list, data, company_currency)
report_summary = get_report_summary(summary_data, company_currency)
@@ -411,13 +411,12 @@ def get_report_summary(summary_data, currency):
return report_summary
def get_chart_data(columns, data, currency):
labels = [d.get("label") for d in columns[2:]]
print(data)
def get_chart_data(period_list, data, currency):
labels = [period.get("label") for period in period_list]
datasets = [
{
"name": section.get("section").replace("'", ""),
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
"values": [section.get(period.get("key")) for period in period_list],
}
for section in data
if section.get("parent_section") is None and section.get("currency")

View File

@@ -48,22 +48,25 @@ def execute(filters=None):
return columns, data, message, chart
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
companies_column, companies = get_companies(filters)
columns = get_columns(companies_column, filters)
company_list, companies = get_companies(filters)
company_columns = get_company_columns(company_list, filters)
columns = get_columns(company_columns)
if filters.get("report") == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(
fiscal_year, companies, columns, filters
fiscal_year, companies, company_columns, filters
)
elif filters.get("report") == "Profit and Loss Statement":
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters)
data, message, chart, report_summary = get_profit_loss_data(
fiscal_year, companies, company_columns, filters
)
else:
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary
def get_balance_sheet_data(fiscal_year, companies, columns, filters):
def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
@@ -116,7 +119,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
True,
)
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
return data, message, chart, report_summary
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):
def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
if net_profit_loss:
data.append(net_profit_loss)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True
@@ -279,7 +282,30 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
return data
def get_columns(companies, filters):
def get_company_columns(companies, filters):
company_columns = []
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
company_columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return company_columns
def get_columns(company_columns):
columns = [
{
"fieldname": "account",
@@ -297,23 +323,7 @@ def get_columns(companies, filters):
},
]
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
columns.extend(company_columns)
return columns

View File

@@ -37,6 +37,20 @@ function get_filters() {
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Link",
options: "Party Type",
width: 100,
},
{
fieldname: "party",
label: __("Party"),
fieldtype: "Dynamic Link",
options: "party_type",
width: 100,
},
{
fieldname: "voucher_no",
label: __("Voucher No"),

View File

@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(gle.party.eq(self.filters.party))
if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else:
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(ple.party.eq(self.filters.party))
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(

View File

@@ -5,15 +5,16 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
from frappe.query_builder import Order
from frappe.query_builder import Case, Order
from frappe.query_builder.functions import Coalesce
from frappe.utils import cint, flt, formatdate
from pypika.terms import ExistsCriterion
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import get_incoming_rate
@@ -176,7 +177,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
column_names = get_column_names()
# to display item as Item Code: Item Name
columns[0] = "Sales Invoice:Link/Item:300"
columns[0]["fieldname"] = "sales_invoice"
columns[0]["options"] = "Item"
columns[0]["width"] = 300
# removing Item Code and Item Name columns
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row)
total_gross_profit = total_base_amount - total_buying_amount
total_gross_profit = flt(
total_base_amount + abs(total_buying_amount)
if total_buying_amount < 0
else total_base_amount - total_buying_amount,
)
data.append(
frappe._dict(
{
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
"buying_amount": total_buying_amount,
"gross_profit": total_gross_profit,
"gross_profit_%": flt(
(total_gross_profit / total_base_amount) * 100.0,
(total_gross_profit / abs(total_base_amount)) * 100.0,
cint(frappe.db.get_default("currency_precision")) or 3,
)
if total_base_amount
@@ -248,9 +255,13 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
data.append(row)
total_gross_profit = total_base_amount - total_buying_amount
total_gross_profit = flt(
total_base_amount + abs(total_buying_amount)
if total_buying_amount < 0
else total_base_amount - total_buying_amount,
)
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
gross_profit_percent = (total_gross_profit / abs(total_base_amount) * 100.0) if total_base_amount else 0
total_row = {
group_columns[0]: "Total",
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
base_amount += row.base_amount
# calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
row.gross_profit = flt(
row.base_amount + abs(row.buying_amount)
if row.buying_amount < 0
else row.base_amount - row.buying_amount,
self.currency_precision,
)
if row.base_amount:
row.gross_profit_percent = flt(
(row.gross_profit / row.base_amount) * 100.0,
(row.gross_profit / abs(row.base_amount)) * 100.0,
self.currency_precision,
)
else:
@@ -633,7 +649,7 @@ class GrossProfitGenerator:
new_row = row
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
else:
new_row.qty += flt(row.qty)
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
new_row = self.set_average_rate(new_row)
@@ -643,11 +659,17 @@ class GrossProfitGenerator:
if i == 0:
new_row = row
else:
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
new_row.buying_amount = flt(
(new_row.buying_amount + row.buying_amount), self.currency_precision
)
new_row.base_amount = flt(
(new_row.base_amount + row.base_amount), self.currency_precision
)
if self.filters.get("group_by") == "Sales Person":
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
new_row.allocated_amount = flt(
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
@@ -673,9 +695,14 @@ class GrossProfitGenerator:
return new_row
def set_average_gross_profit(self, new_row):
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
new_row.gross_profit = flt(
new_row.base_amount + abs(new_row.buying_amount)
if new_row.buying_amount < 0
else new_row.base_amount - new_row.buying_amount,
self.currency_precision,
)
new_row.gross_profit_percent = (
flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision)
flt(((new_row.gross_profit / abs(new_row.base_amount)) * 100.0), self.currency_precision)
if new_row.base_amount
else 0
)
@@ -851,129 +878,173 @@ class GrossProfitGenerator:
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
def load_invoice_items(self):
conditions = ""
if self.filters.company:
conditions += " and `tabSales Invoice`.company = %(company)s"
if self.filters.from_date:
conditions += " and posting_date >= %(from_date)s"
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
self.si_list = []
SalesInvoice = frappe.qb.DocType("Sales Invoice")
base_query = self.prepare_invoice_query()
if self.filters.include_returned_invoices:
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
invoice_query = base_query.where(
(SalesInvoice.is_return == 0)
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
)
else:
conditions += " and is_return = 0"
invoice_query = base_query.where(SalesInvoice.is_return == 0)
if self.filters.item_group:
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
self.si_list += invoice_query.run(as_dict=True)
self.prepare_vouchers_to_ignore()
if self.filters.sales_person:
conditions += """
and exists(select 1
from `tabSales Team` st
where st.parent = `tabSales Invoice`.name
and st.sales_person = %(sales_person)s)
"""
ret_invoice_query = base_query.where(
(SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
)
if self.vouchers_to_ignore:
ret_invoice_query = ret_invoice_query.where(
SalesInvoice.return_against.notin(self.vouchers_to_ignore)
)
self.si_list += ret_invoice_query.run(as_dict=True)
def prepare_invoice_query(self):
SalesInvoice = frappe.qb.DocType("Sales Invoice")
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
Item = frappe.qb.DocType("Item")
SalesTeam = frappe.qb.DocType("Sales Team")
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
query = (
frappe.qb.from_(SalesInvoice)
.join(SalesInvoiceItem)
.on(SalesInvoiceItem.parent == SalesInvoice.name)
.join(Item)
.on(Item.name == SalesInvoiceItem.item_code)
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
)
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item)
query = query.select(
SalesInvoiceItem.parenttype,
SalesInvoiceItem.parent,
SalesInvoice.posting_date,
SalesInvoice.posting_time,
SalesInvoice.project,
SalesInvoice.update_stock,
SalesInvoice.customer,
SalesInvoice.customer_group,
SalesInvoice.customer_name,
SalesInvoice.territory,
SalesInvoiceItem.item_code,
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
SalesInvoiceItem.item_name,
SalesInvoiceItem.description,
SalesInvoiceItem.warehouse,
SalesInvoiceItem.item_group,
SalesInvoiceItem.brand,
SalesInvoiceItem.so_detail,
SalesInvoiceItem.sales_order,
SalesInvoiceItem.dn_detail,
SalesInvoiceItem.delivery_note,
SalesInvoiceItem.stock_qty.as_("qty"),
SalesInvoiceItem.base_net_rate,
SalesInvoiceItem.base_net_amount,
SalesInvoiceItem.name.as_("item_row"),
SalesInvoice.is_return,
SalesInvoiceItem.cost_center,
SalesInvoiceItem.serial_and_batch_bundle,
)
if self.filters.group_by == "Sales Person":
sales_person_cols = """, sales.sales_person,
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
sales.incentives
"""
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
else:
sales_person_cols = ""
sales_team_table = ""
query = query.select(
SalesTeam.sales_person,
(SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
"allocated_amount"
),
SalesTeam.incentives,
)
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
if self.filters.group_by == "Payment Term":
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
'{}',
coalesce(schedule.payment_term, '{}')) as payment_term,
schedule.invoice_portion,
schedule.payment_amount """.format(_("Sales Return"), _("No Terms"))
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
`tabSales Invoice`.is_return = 0 """
else:
payment_term_cols = ""
payment_term_table = ""
query = query.select(
Case()
.when(SalesInvoice.is_return == 1, _("Sales Return"))
.else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
.as_("payment_term"),
PaymentSchedule.invoice_portion,
PaymentSchedule.payment_amount,
)
if self.filters.get("sales_invoice"):
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
query = query.left_join(PaymentSchedule).on(
(PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
)
if self.filters.get("item_code"):
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
SalesInvoice.posting_time, order=Order.desc
)
if self.filters.get("cost_center"):
return query
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item):
if self.filters.company:
query = query.where(SalesInvoice.company == self.filters.company)
if self.filters.from_date:
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
if self.filters.to_date:
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
if self.filters.item_group:
query = query.where(get_item_group_condition(self.filters.item_group, Item))
if self.filters.sales_person:
query = query.where(
ExistsCriterion(
frappe.qb.from_(SalesTeam)
.select(1)
.where(
(SalesTeam.parent == SalesInvoice.name)
& (SalesTeam.sales_person == self.filters.sales_person)
)
)
)
if self.filters.sales_invoice:
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
if self.filters.item_code:
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
if self.filters.cost_center:
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s"
query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
if self.filters.get("project"):
if self.filters.project:
self.filters.project = frappe.parse_json(self.filters.get("project"))
conditions += " and `tabSales Invoice Item`.project in %(project)s"
query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
accounting_dimensions = get_accounting_dimensions(as_list=False)
if accounting_dimensions:
for dimension in accounting_dimensions:
if self.filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
self.filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, self.filters.get(dimension.fieldname)
)
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
else:
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
for dim in get_accounting_dimensions(as_list=False) or []:
if self.filters.get(dim.fieldname):
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
self.filters[dim.fieldname] = get_dimension_with_children(
dim.document_type, self.filters.get(dim.fieldname)
)
query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
if self.filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
if self.filters.warehouse:
lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
WH = frappe.qb.DocType("Warehouse")
query = query.where(
SalesInvoiceItem.warehouse.isin(
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
)
)
if warehouse_details:
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
self.si_list = frappe.db.sql(
"""
select
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
{sales_person_cols}
{payment_term_cols}
from
`tabSales Invoice` inner join `tabSales Invoice Item`
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
{sales_team_table}
{payment_term_table}
where
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
order by
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format(
conditions=conditions,
sales_person_cols=sales_person_cols,
sales_team_table=sales_team_table,
payment_term_cols=payment_term_cols,
payment_term_table=payment_term_table,
match_cond=get_match_cond("Sales Invoice"),
),
self.filters,
as_dict=1,
)
return query
def prepare_vouchers_to_ignore(self):
self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
def get_delivery_notes(self):
self.delivery_notes = frappe._dict({})

View File

@@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase):
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
sinv = sinv.save().submit()
filters = frappe._dict(
@@ -465,7 +466,7 @@ class TestGrossProfit(FrappeTestCase):
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
"gross_profit_%": -100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])
@@ -642,21 +643,24 @@ class TestGrossProfit(FrappeTestCase):
def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
sales_inv_date = month_start_date
return_inv_date = add_days(month_end_date, 1)
# create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = month_start_date
sinv.posting_date = sales_inv_date
sinv.save().submit()
# create credit note on next month start date
cr_note = make_sales_return(sinv.name)
cr_note.set_posting_time = 1
cr_note.posting_date = add_days(month_end_date, 1)
cr_note.posting_date = return_inv_date
cr_note.save().submit()
# apply filters for invoiced period
filters = frappe._dict(
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
company=self.company, from_date=month_start_date, to_date=month_start_date, group_by="Invoice"
)
_, data = execute(filters=filters)
@@ -668,7 +672,7 @@ class TestGrossProfit(FrappeTestCase):
self.assertEqual(total.get("gross_profit_%"), 100.0)
# extend filters upto returned period
filters.update(to_date=add_days(month_end_date, 1))
filters.update({"to_date": return_inv_date})
_, data = execute(filters=filters)
total = data[-1]
@@ -677,3 +681,63 @@ class TestGrossProfit(FrappeTestCase):
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 0.0)
self.assertEqual(total.get("gross_profit_%"), 0.0)
# apply filters only on returned period
filters.update({"from_date": return_inv_date, "to_date": return_inv_date})
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, -100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, -100.0)
self.assertEqual(total.get("gross_profit_%"), -100.0)
def test_sales_person_wise_gross_profit(self):
sales_person = make_sales_person("_Test Sales Person")
posting_date = get_first_day(nowdate())
qty = 10
rate = 100
sinv = self.create_sales_invoice(qty=qty, rate=rate, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = posting_date
sinv.append(
"sales_team",
{
"sales_person": sales_person.name,
"allocated_percentage": 100,
"allocated_amount": 1000.0,
"commission_rate": 5,
"incentives": 5,
},
)
sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=posting_date, to_date=posting_date, group_by="Sales Person"
)
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total[5], 1000.0)
self.assertEqual(total[6], 0.0)
self.assertEqual(total[7], 1000.0)
self.assertEqual(total[8], 100.0)
def make_sales_person(sales_person_name="_Test Sales Person"):
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
sales_person_doc = frappe.get_doc(
{
"doctype": "Sales Person",
"is_group": 0,
"parent_sales_person": "Sales Team",
"sales_person_name": sales_person_name,
}
).insert(ignore_permissions=True)
else:
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
return sales_person_doc

View File

@@ -62,7 +62,7 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
@@ -158,18 +158,20 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
return net_profit_loss
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[2:]]
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
labels = [col.get("label") for col in chart_columns]
income_data, expense_data, net_profit = [], [], []
for p in columns[2:]:
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if income:
income_data.append(income[-2].get(p.get("fieldname")))
income_data.append(income[-2].get(key))
if expense:
expense_data.append(expense[-2].get(p.get("fieldname")))
expense_data.append(expense[-2].get(key))
if net_profit_loss:
net_profit.append(net_profit_loss.get(p.get("fieldname")))
net_profit.append(net_profit_loss.get(key))
datasets = []
if income_data:

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.query_builder import Criterion
from frappe.utils import cstr, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -33,11 +34,19 @@ def execute(filters=None):
def get_accounts_data(based_on, company):
if based_on == "Cost Center":
return frappe.db.sql(
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
from `tabCost Center` where company=%s order by name""",
company,
as_dict=True,
cc = qb.DocType("Cost Center")
return (
qb.from_(cc)
.select(
cc.name,
cc.parent_cost_center.as_("parent_account"),
cc.cost_center_name.as_("account_name"),
cc.lft,
cc.rgt,
)
.where(cc.company.eq(company))
.orderby(cc.name)
.run(as_dict=True)
)
elif based_on == "Project":
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
):
"""Returns a dict like { "account": [gl entries], ... }"""
additional_conditions = []
gl = qb.DocType("GL Entry")
acc = qb.DocType("Account")
conditions = []
conditions.append(gl.company.eq(company))
conditions.append(gl[based_on].notnull())
conditions.append(gl.is_cancelled.eq(0))
if from_date and to_date:
conditions.append(gl.posting_date.between(from_date, to_date))
elif from_date and not to_date:
conditions.append(gl.posting_date.gte(from_date))
elif not from_date and to_date:
conditions.append(gl.posting_date.lte(to_date))
if ignore_closing_entries:
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
if from_date:
additional_conditions.append("and posting_date >= %(from_date)s")
gl_entries = frappe.db.sql(
"""select posting_date, {based_on} as based_on, debit, credit,
is_opening, (select root_type from `tabAccount` where name = account) as type
from `tabGL Entry` where company=%(company)s
{additional_conditions}
and posting_date <= %(to_date)s
and {based_on} is not null
and is_cancelled = 0
order by {based_on}, posting_date""".format(
additional_conditions="\n".join(additional_conditions), based_on=based_on
),
{"company": company, "from_date": from_date, "to_date": to_date},
as_dict=True,
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
gl_entries = (
qb.from_(gl)
.select(
gl.posting_date,
gl[based_on].as_("based_on"),
gl.debit,
gl.credit,
gl.is_opening,
root_subquery.as_("type"),
)
.where(Criterion.all(conditions))
.orderby(gl[based_on], gl.posting_date)
.run(as_dict=True)
)
for entry in gl_entries:

View File

@@ -51,7 +51,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
entries = {}
for name, details in gle_map.items():
for entry in details:
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0
tax_withholding_category, rate = None, None
bill_no, bill_date = "", ""
party = entry.party or entry.against
@@ -83,6 +83,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
total_amount = grand_total = base_total
base_tax_withholding_net_total = total_amount
else:
if tax_amount and rate:
@@ -93,12 +94,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
grand_total = values[1]
base_total = values[2]
base_tax_withholding_net_total = total_amount
if voucher_type == "Purchase Invoice":
base_tax_withholding_net_total = values[0]
bill_no = values[3]
bill_date = values[4]
else:
total_amount += entry.credit
base_tax_withholding_net_total = total_amount
if tax_amount:
if party_map.get(party, {}).get("party_type") == "Supplier":
@@ -125,6 +130,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
"rate": rate,
"total_amount": total_amount,
"grand_total": grand_total,
"base_tax_withholding_net_total": base_tax_withholding_net_total,
"base_total": base_total,
"tax_amount": tax_amount,
"transaction_date": posting_date,
@@ -252,14 +258,14 @@ def get_columns(filters):
"width": 60,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"label": _("Tax Withholding Net Total"),
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Float",
"width": 120,
"width": 150,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
@@ -270,10 +276,16 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Grand Total"),
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Float",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 120,
"width": 170,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
{

View File

@@ -35,9 +35,9 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
[jv.name, "TCS", 0.075, -10000.0, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 706.67, 2550.0, 0.53, 2550.53],
[si.name, "TCS", 0.075, 693.33, 1000.0, 0.52, 1000.52],
]
self.check_expected_values(result, expected_values)
@@ -55,8 +55,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
)[1]
expected_values = [
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
[inv_1.name, "TDS - 1", 10, 5000, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 5000, 1000, 6000],
]
self.check_expected_values(result, expected_values)
@@ -107,8 +107,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
)[1]
expected_values = [
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
[inv_1.name, "TDS - 3", 10.0, 5000, 5000, 500, 4500],
[inv_2.name, "TDS - 3", 20.0, 5000, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
@@ -120,6 +120,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
voucher.ref_no,
voucher.section_code,
voucher.rate,
voucher.base_tax_withholding_net_total,
voucher.base_total,
voucher.tax_amount,
voucher.grand_total,

View File

@@ -128,7 +128,7 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Total Amount"),
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,

View File

@@ -112,6 +112,12 @@ frappe.query_reports["Trial Balance"] = {
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_group_accounts",
label: __("Show Group Accounts"),
fieldtype: "Check",
default: 1,
},
],
formatter: erpnext.financial_statements.formatter,
tree: true,

View File

@@ -83,7 +83,7 @@ def validate_filters(filters):
def get_data(filters):
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
@@ -345,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
def calculate_total_row(data, company_currency, show_group_accounts=True):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
@@ -362,10 +362,16 @@ def calculate_total_row(accounts, company_currency):
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
def sum_value_fields(row):
for field in value_fields:
total_row[field] += row[field]
for d in data:
if not show_group_accounts:
sum_value_fields(d)
elif show_group_accounts and not d.get("parent_account"):
sum_value_fields(d)
return total_row
@@ -393,6 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
"from_date": filters.from_date,
"to_date": filters.to_date,
"currency": company_currency,
"is_group_account": d.is_group,
"account_name": (
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
),
@@ -408,7 +415,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
row["has_value"] = has_value
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
if not filters.get("show_group_accounts"):
data = hide_group_accounts(data)
total_row = calculate_total_row(
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
)
data.extend([{}, total_row])
return data
@@ -488,3 +501,12 @@ def prepare_opening_closing(row):
row[valid_col] = 0.0
else:
row[reverse_col] = 0.0
def hide_group_accounts(data):
non_group_accounts_data = []
for d in data:
if not d.get("is_group_account"):
d.update(indent=0)
non_group_accounts_data.append(d)
return non_group_accounts_data

View File

@@ -454,7 +454,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
dimensions_dict = frappe._dict()
if entry and active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
if entry_dimension := entry.get(dim.fieldname):
dimensions_dict[dim.fieldname] = entry_dimension
return dimensions_dict

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_link_to_form
from frappe.utils import cstr, get_datetime, get_link_to_form
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -34,6 +34,7 @@ class AssetMovement(Document):
for d in self.assets:
self.validate_asset(d)
self.validate_movement(d)
self.validate_transaction_date(d)
def validate_asset(self, d):
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
@@ -51,6 +52,18 @@ class AssetMovement(Document):
else:
self.validate_employee(d)
def validate_transaction_date(self, d):
previous_movement_date = frappe.db.get_value(
"Asset Movement",
[["Asset Movement Item", "asset", "=", d.asset], ["docstatus", "=", 1]],
"transaction_date",
order_by="transaction_date desc",
)
if previous_movement_date and get_datetime(previous_movement_date) > get_datetime(
self.transaction_date
):
frappe.throw(_("Transaction date can't be earlier than previous movement date"))
def validate_location_and_employee(self, d):
self.validate_location(d)
self.validate_employee(d)

View File

@@ -4,9 +4,9 @@
import unittest
import frappe
from frappe.utils import now
from frappe.utils import add_days, now
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.setup.doctype.employee.test_employee import make_employee
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -147,6 +147,33 @@ class TestAssetMovement(unittest.TestCase):
movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
def test_movement_transaction_date(self):
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.save().submit()
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
asset_creation_date = frappe.db.get_value(
"Asset Movement",
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
"transaction_date",
)
asset_movement = create_asset_movement(
purpose="Transfer",
company=asset.company,
assets=[
{
"asset": asset.name,
"source_location": "Test Location",
"target_location": "Test Location 2",
}
],
transaction_date=add_days(asset_creation_date, -1),
do_not_save=True,
)
self.assertRaises(frappe.ValidationError, asset_movement.save)
def create_asset_movement(**args):
args = frappe._dict(args)
@@ -165,9 +192,10 @@ def create_asset_movement(**args):
"reference_name": args.reference_name,
}
)
movement.insert()
movement.submit()
if not args.do_not_save:
movement.insert(ignore_if_duplicate=True)
if not args.do_not_submit:
movement.submit()
return movement

View File

@@ -111,16 +111,12 @@ frappe.ui.form.on("Asset Repair", {
purchase_invoice: function (frm) {
if (frm.doc.purchase_invoice) {
frappe.call({
method: "frappe.client.get_value",
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice",
args: {
doctype: "Purchase Invoice",
fieldname: "base_net_total",
filters: { name: frm.doc.purchase_invoice },
purchase_invoice: frm.doc.purchase_invoice,
},
callback: function (r) {
if (r.message) {
frm.set_value("repair_cost", r.message.base_net_total);
}
frm.set_value("repair_cost", r.message || 0);
},
});
} else {

View File

@@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
@@ -308,9 +309,14 @@ class AssetRepair(AccountsController):
if flt(self.repair_cost) <= 0:
return
pi_expense_account = (
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
)
expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice)
if not expense_accounts:
frappe.throw(
_("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice)
)
pi_expense_account = expense_accounts[0]
gl_entries.append(
self.get_gl_dict(
@@ -473,3 +479,84 @@ class AssetRepair(AccountsController):
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2)
@frappe.whitelist()
def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float:
"""
Get the total repair cost from GL entries for a purchase invoice.
Only considers expense accounts for non-stock, non-fixed-asset items.
"""
if not purchase_invoice:
return 0.0
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
if not expense_accounts:
return 0.0
return _get_total_expense_amount(purchase_invoice, expense_accounts)
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
"""
Get expense accounts for non-stock items from the purchase invoice.
"""
pi_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": purchase_invoice},
fields=["item_code", "expense_account", "is_fixed_asset"],
)
if not pi_items:
return []
# Get list of stock item codes from the invoice
item_codes = {item.item_code for item in pi_items if item.item_code}
stock_items = set()
if item_codes:
stock_items = set(
frappe.db.get_all(
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
)
)
expense_accounts = set()
for item in pi_items:
# Skip stock items - they use warehouse accounts
if item.item_code and item.item_code in stock_items:
continue
# Skip fixed assets - they use asset accounts
if item.is_fixed_asset:
continue
# Use expense account from Purchase Invoice Item
if item.expense_account:
expense_accounts.add(item.expense_account)
return list(expense_accounts)
def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float:
"""Get the total expense amount from GL entries for a purchase invoice and accounts."""
if not expense_accounts:
return 0.0
gl_entry = frappe.qb.DocType("GL Entry")
result = (
frappe.qb.from_(gl_entry)
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
.where(
(gl_entry.voucher_type == "Purchase Invoice")
& (gl_entry.voucher_no == purchase_invoice)
& (gl_entry.account.isin(expense_accounts))
& (gl_entry.is_cancelled == 0)
)
).run(as_dict=True)
return flt(result[0].total) if result else 0.0

View File

@@ -8,6 +8,7 @@ from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import (
get_asset_account,
get_asset_value_after_depreciation,
@@ -21,6 +22,7 @@ from erpnext.assets.doctype.asset.test_asset import (
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
@@ -321,6 +323,59 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def test_repair_cost_fetches_only_service_item_amount(self):
"""Test that repair cost only includes service (non-stock) item amounts from purchase invoice."""
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
service_item = create_item(
"_Test Service Item for Repair",
is_stock_item=0,
warehouse=warehouse,
company=company,
)
stock_item = create_item(
"_Test Stock Item for Repair",
is_stock_item=1,
warehouse=warehouse,
company=company,
)
service_expense_account = "Miscellaneous Expenses - TCP1"
cost_center = frappe.db.get_value("Company", company, "cost_center")
pi = make_purchase_invoice(
item_code=service_item.name,
qty=1,
rate=500,
expense_account=service_expense_account,
cost_center=cost_center,
warehouse=warehouse,
update_stock=0,
do_not_submit=1,
company=company,
)
pi.update_stock = 1
pi.append(
"items",
{
"item_code": stock_item.name,
"qty": 2,
"rate": 300,
"warehouse": warehouse,
"cost_center": cost_center,
},
)
pi.save()
pi.submit()
repair_cost = get_repair_cost_for_purchase_invoice(pi.name)
self.assertEqual(repair_cost, 500)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations
@@ -411,6 +466,7 @@ def create_asset_repair(**args):
if asset.calculate_depreciation:
asset_repair.increase_in_asset_life = 12
pi = make_purchase_invoice(
item=args.item or "_Test Non Stock Item",
company=asset.company,
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
cost_center=asset_repair.cost_center,

View File

@@ -196,6 +196,9 @@ class PurchaseOrder(BuyingController):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
if self.is_subcontracted:
self.status_updater[0]["source_field"] = "fg_item_qty"
def validate(self):
super().validate()

View File

@@ -250,10 +250,17 @@ frappe.ui.form.on("Request for Quotation", {
"subject",
])
.then((r) => {
frm.set_value(
"message_for_supplier",
r.message.use_html ? r.message.response_html : r.message.response
);
if (r.message.use_html) {
frm.set_value({
mfs_html: r.message.response_html,
use_html: 1,
});
} else {
frm.set_value({
message_for_supplier: r.message.response,
use_html: 0,
});
}
frm.set_value("subject", r.message.subject);
});
}

View File

@@ -31,7 +31,9 @@
"send_document_print",
"sec_break_email_2",
"subject",
"use_html",
"message_for_supplier",
"mfs_html",
"terms_section_break",
"incoterm",
"named_place",
@@ -142,12 +144,13 @@
{
"allow_on_submit": 1,
"default": "Please supply the specified items at the best possible rates",
"depends_on": "eval:doc.use_html == 0",
"fieldname": "message_for_supplier",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Message for Supplier",
"print_hide": 1,
"reqd": 1
"mandatory_depends_on": "eval:doc.use_html == 0",
"print_hide": 1
},
{
"collapsible": 1,
@@ -324,6 +327,22 @@
"label": "Subject",
"not_nullable": 1,
"reqd": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.use_html == 1",
"fieldname": "mfs_html",
"fieldtype": "Code",
"label": "Message for Supplier",
"mandatory_depends_on": "eval:doc.use_html == 1",
"print_hide": 1
},
{
"default": "0",
"fieldname": "use_html",
"fieldtype": "Check",
"hidden": 1,
"label": "Use HTML"
}
],
"grid_page_length": 50,
@@ -331,7 +350,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-05 14:27:33.329810",
"modified": "2026-03-01 23:38:48.079274",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -47,7 +47,8 @@ class RequestforQuotation(BuyingController):
incoterm: DF.Link | None
items: DF.Table[RequestforQuotationItem]
letter_head: DF.Link | None
message_for_supplier: DF.TextEditor
message_for_supplier: DF.TextEditor | None
mfs_html: DF.Code | None
named_place: DF.Data | None
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
opportunity: DF.Link | None
@@ -61,6 +62,7 @@ class RequestforQuotation(BuyingController):
tc_name: DF.Link | None
terms: DF.TextEditor | None
transaction_date: DF.Date
use_html: DF.Check
vendor: DF.Link | None
# end: auto-generated types
@@ -100,8 +102,16 @@ class RequestforQuotation(BuyingController):
["use_html", "response", "response_html", "subject"],
as_dict=True,
)
if not self.message_for_supplier:
self.message_for_supplier = data.response_html if data.use_html else data.response
self.use_html = data.use_html
if data.use_html:
if not self.mfs_html:
self.mfs_html = data.response_html
else:
if not self.message_for_supplier:
self.message_for_supplier = data.response
if not self.subject:
self.subject = data.subject
@@ -304,12 +314,20 @@ class RequestforQuotation(BuyingController):
else:
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
message_template = self.mfs_html if self.use_html else self.message_for_supplier
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
rendered_message = frappe.render_template(message_template, doc_args)
subject_source = (
self.subject
or frappe.get_value("Email Template", self.email_template, "subject")
or _("Request for Quotation")
)
rendered_subject = frappe.render_template(subject_source, doc_args)
if preview:
return {
"message": self.message_for_supplier,
"subject": self.subject
or frappe.get_value("Email Template", self.email_template, "subject")
or _("Request for Quotation"),
"message": rendered_message,
"subject": rendered_subject,
}
attachments = []
@@ -333,10 +351,8 @@ class RequestforQuotation(BuyingController):
self.send_email(
data,
sender,
self.subject
or frappe.get_value("Email Template", self.email_template, "subject")
or _("Request for Quotation"),
self.message_for_supplier,
rendered_subject,
rendered_message,
attachments,
)

View File

@@ -139,14 +139,6 @@ frappe.ui.form.on("Supplier", {
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
frm.set_query("supplier_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
get_supplier_group_details: function (frm) {
frappe.call({

View File

@@ -165,6 +165,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Supplier Group",
"link_filters": "[[\"Supplier Group\",\"is_group\",\"=\",0]]",
"oldfieldname": "supplier_type",
"oldfieldtype": "Link",
"options": "Supplier Group"
@@ -485,7 +486,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-05-08 18:02:57.342931",
"modified": "2026-02-06 12:58:01.398824",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -551,4 +552,4 @@
"states": [],
"title_field": "supplier_name",
"track_changes": 1
}
}

View File

@@ -30,6 +30,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
child_docname: "items",
cannot_add_row: false,
});
});
} else if (this.frm.doc.docstatus === 0) {
erpnext.set_unit_price_items_note(this.frm);

View File

@@ -345,3 +345,15 @@ def set_expired_status():
""",
(nowdate()),
)
def get_purchased_items(supplier_quotation: str):
return frappe._dict(
frappe.get_all(
"Purchase Order Item",
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
fields=["supplier_quotation_item", "sum(qty)"],
group_by="supplier_quotation_item",
as_list=1,
)
)

View File

@@ -2,15 +2,131 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
class TestPurchaseOrder(FrappeTestCase):
def test_update_child_supplier_quotation_add_item(self):
sq = frappe.copy_doc(test_records[0])
sq.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": 5,
"docname": sq.items[0].name,
},
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
},
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(sq.get("items")[0].qty, 5)
self.assertEqual(sq.get("items")[1].rate, 300)
def test_update_supplier_quotation_child_rate(self):
sq = frappe.copy_doc(test_records[0])
sq.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": 300,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(sq.get("items")[0].rate, 300)
po = make_purchase_order(sq.name)
po.schedule_date = add_days(today(), 1)
po.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": 20,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
]
)
self.assertRaises(
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
def test_update_supplier_quotation_child_remove_item(self):
sq = frappe.copy_doc(test_records[0])
sq.submit()
po = make_purchase_order(sq.name)
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
},
]
)
po.get("items")[0].schedule_date = add_days(today(), 1)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
po.submit()
sq.reload()
trans_item = json.dumps(
[
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
}
]
)
frappe.db.savepoint("before_cancel")
# check if item having purchase order can be removed
self.assertRaises(
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
frappe.db.rollback(save_point="before_cancel")
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
}
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(len(sq.get("items")), 1)
def test_supplier_quotation_qty(self):
sq = frappe.copy_doc(test_records[0])
sq.items[0].qty = 0

View File

@@ -165,7 +165,7 @@ def get_data(filters):
"cost_center": po.cost_center,
"project": po.project,
"requesting_site": po.warehouse,
"requestor": po.owner,
"requestor": mr_record.get("owner", po.owner),
"material_request_no": po.material_request,
"item_code": po.item_code,
"quantity": flt(po.qty),

View File

@@ -2580,12 +2580,12 @@ class AccountsController(TransactionBase):
def get_order_details(self):
if self.doctype == "Sales Invoice":
po_or_so = self.get("items")[0].get("sales_order")
po_or_so = self.get("items") and self.get("items")[0].get("sales_order")
po_or_so_doctype = "Sales Order"
po_or_so_doctype_name = "sales_order"
else:
po_or_so = self.get("items")[0].get("purchase_order")
po_or_so = self.get("items") and self.get("items")[0].get("purchase_order")
po_or_so_doctype = "Purchase Order"
po_or_so_doctype_name = "purchase_order"
@@ -3678,7 +3678,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
if child_doctype == "Purchase Order Item":
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
# Initialized value will update in parent validation
child_item.base_rate = 1
child_item.base_amount = 1
@@ -3696,7 +3696,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
return child_item
def validate_child_on_delete(row, parent):
def validate_child_on_delete(row, parent, ordered_item=None):
"""Check if partially transacted item (row) is being deleted."""
if parent.doctype == "Sales Order":
if flt(row.delivered_qty):
@@ -3724,13 +3724,17 @@ def validate_child_on_delete(row, parent):
row.idx, row.item_code
)
)
if flt(row.billed_amt):
frappe.throw(
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
row.idx, row.item_code
if parent.doctype in ["Purchase Order", "Sales Order"]:
if flt(row.billed_amt):
frappe.throw(
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
row.idx, row.item_code
)
)
)
if parent.doctype == "Quotation":
if ordered_item.get(row.name):
frappe.throw(_("Cannot delete an item which has been ordered"))
def update_bin_on_delete(row, doctype):
@@ -3756,7 +3760,7 @@ def update_bin_on_delete(row, doctype):
update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data) -> bool:
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
for item in parent.items:
@@ -3764,7 +3768,7 @@ def validate_and_delete_children(parent, data) -> bool:
deleted_children.append(item)
for d in deleted_children:
validate_child_on_delete(d, parent)
validate_child_on_delete(d, parent, ordered_item)
d.cancel()
d.delete()
@@ -3773,16 +3777,19 @@ def validate_and_delete_children(parent, data) -> bool:
# need to update ordered qty in Material Request first
# bin uses Material Request Items to recalculate & update
parent.update_prevdoc_status()
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
parent.update_prevdoc_status()
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
return bool(deleted_children)
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
def check_doc_permissions(doc, perm_type="create"):
try:
doc.check_permission(perm_type)
@@ -3821,7 +3828,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
def get_new_child_item(item_row):
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
child_doctype = parent_doctype + " Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def is_allowed_zero_qty():
@@ -3831,20 +3838,51 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
return False
def validate_quantity(child_item, new_data):
def validate_quantity_and_rate(child_item, new_data):
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
frappe.throw(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
)
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
qty_limits = {
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
}
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
frappe.throw(_("Cannot set quantity less than received quantity"))
if parent_doctype in qty_limits:
qty_field, error_message = qty_limits[parent_doctype]
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
frappe.throw(
_("Row #{0}:").format(new_data.get("idx"))
+ error_message.format(frappe.bold(new_data.get("item_code"))),
title=_("Invalid Qty"),
)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if (parent_doctype == "Quotation" and not ordered_items) or (
parent_doctype == "Supplier Quotation" and not purchased_items
):
return
qty_to_check = (
ordered_items.get(child_item.name)
if parent_doctype == "Quotation"
else purchased_items.get(child_item.name)
)
if qty_to_check:
if not rate_unchanged:
frappe.throw(
_(
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
).format(frappe.bold(new_data.get("item_code")))
)
if flt(new_data.get("qty")) < qty_to_check:
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
def should_update_supplied_items(doc) -> bool:
"""Subcontracted PO can allow following changes *after submit*:
@@ -3888,7 +3926,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
data = json.loads(trans_items)
any_qty_changed = False # updated to true if any item's qty changes
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
@@ -3896,7 +3933,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
_removed_items = validate_and_delete_children(parent, data)
if parent_doctype == "Quotation":
ordered_items = get_ordered_items(parent.name)
_removed_items = validate_and_delete_children(parent, data, ordered_items)
elif parent_doctype == "Supplier Quotation":
purchased_items = get_purchased_items(parent.name)
_removed_items = validate_and_delete_children(parent, data, purchased_items)
else:
_removed_items = validate_and_delete_children(parent, data)
items_added_or_removed |= _removed_items
for d in data:
@@ -3936,7 +3982,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
conversion_factor_unchanged = prev_con_fac == new_con_fac
any_conversion_factor_changed |= not conversion_factor_unchanged
date_unchanged = (
prev_date == getdate(new_date) if prev_date and new_date else False
(prev_date == getdate(new_date) if prev_date and new_date else False)
if parent_doctype not in ["Quotation", "Supplier Quotation"]
else None
) # in case of delivery note etc
if (
rate_unchanged
@@ -3948,7 +3996,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
):
continue
validate_quantity(child_item, d)
validate_quantity_and_rate(child_item, d)
if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True
@@ -3972,18 +4021,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
rate_unchanged = prev_rate == new_rate
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
# Amount cannot be lesser than billed amount, except for negative amounts
row_rate = flt(d.get("rate"), rate_precision)
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
row_rate * flt(d.get("qty"), qty_precision), rate_precision
)
if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_(
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
).format(child_item.idx, child_item.item_code)
if parent_doctype in ["Purchase Order", "Sales Order"]:
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
row_rate * flt(d.get("qty"), qty_precision), rate_precision
)
if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_(
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
).format(child_item.idx, child_item.item_code)
)
else:
child_item.rate = row_rate
else:
child_item.rate = row_rate
@@ -4002,6 +4054,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor
)
if child_item.get("total_weight") and child_item.get("weight_per_unit"):
child_item.total_weight = flt(
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
child_item.precision("total_weight"),
)
if d.get("delivery_date") and parent_doctype == "Sales Order":
child_item.delivery_date = d.get("delivery_date")
@@ -4011,26 +4069,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if d.get("bom_no") and parent_doctype == "Sales Order":
child_item.bom_no = d.get("bom_no")
if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
if parent_doctype in ["Sales Order", "Purchase Order"]:
if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:
@@ -4038,7 +4097,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item.idx = len(parent.items) + 1
child_item.insert()
else:
child_item.save()
child_item.save(ignore_permissions=True)
parent.reload()
parent.flags.ignore_validate_update_after_submit = True
@@ -4052,13 +4111,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.doctype, parent.company, parent.base_grand_total
)
parent.set_payment_schedule()
if parent_doctype != "Supplier Quotation":
parent.set_payment_schedule()
if parent_doctype == "Purchase Order":
parent.set_tax_withholding()
parent.validate_minimum_order_qty()
parent.validate_budget()
if parent.is_against_so():
parent.update_status_updater()
else:
elif parent_doctype == "Sales Order":
parent.check_credit_limit()
# reset index of child table
@@ -4091,7 +4152,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
).format(frappe.bold(parent.name))
)
else: # Sales Order
elif parent_doctype == "Sales Order":
parent.validate_selling_price()
parent.validate_for_duplicate_items()
parent.validate_warehouse()
@@ -4103,9 +4164,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.reload()
validate_workflow_conditions(parent)
parent.update_blanket_order()
parent.update_billing_percentage()
parent.set_status()
if parent_doctype in ["Purchase Order", "Sales Order"]:
parent.update_blanket_order()
parent.update_billing_percentage()
parent.set_status()
parent.validate_uom_is_integer("uom", "qty")
parent.validate_uom_is_integer("stock_uom", "stock_qty")

View File

@@ -626,7 +626,9 @@ class BuyingController(SubcontractingController):
or self.is_return
or (self.is_internal_transfer() and self.docstatus == 2)
else self.get_package_for_target_warehouse(
d, type_of_transaction=type_of_transaction
d,
type_of_transaction=type_of_transaction,
via_landed_cost_voucher=via_landed_cost_voucher,
)
),
},
@@ -714,7 +716,22 @@ class BuyingController(SubcontractingController):
via_landed_cost_voucher=via_landed_cost_voucher,
)
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
def get_package_for_target_warehouse(
self, item, warehouse=None, type_of_transaction=None, via_landed_cost_voucher=None
) -> str:
if via_landed_cost_voucher and item.get("warehouse"):
if sabb := frappe.db.get_value(
"Serial and Batch Bundle",
{
"voucher_detail_no": item.name,
"warehouse": item.get("warehouse"),
"docstatus": 1,
"is_cancelled": 0,
},
"name",
):
return sabb
if not item.serial_and_batch_bundle:
return ""

View File

@@ -770,12 +770,34 @@ def get_filters(
if reference_voucher_detail_no:
filters["voucher_detail_no"] = reference_voucher_detail_no
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
filters["warehouse"] = item_row.get("warehouse")
warehouses = []
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
if reference_voucher_detail_no:
warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
filters["warehouse"] = item_row.get("warehouse")
return filters
def get_warehouses_for_return(voucher_type, name):
warehouses = []
warehouse_details = frappe.get_all(
voucher_type + " Item",
filters={"name": name, "docstatus": 1},
fields=["warehouse", "rejected_warehouse"],
)
for d in warehouse_details:
if d.warehouse:
warehouses.append(d.warehouse)
if d.rejected_warehouse:
warehouses.append(d.rejected_warehouse)
return warehouses
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos as get_serial_nos_from_serial_no,
@@ -914,7 +936,14 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
if doctype == "Packed Item":
if key is None:
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
key = frappe.get_cached_value(
"Packed Item",
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
field,
)
if key is None:
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
if row.voucher_type == "Delivery Note":
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
elif row.voucher_type == "Sales Invoice":

View File

@@ -279,7 +279,7 @@ class SellingController(StockController):
_(
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be atleast {4}.<br><br>Alternatively,
you can disable selling price validation in {5} to bypass
you can disable '{5}' in {6} to bypass
this validation."""
).format(
idx,
@@ -287,6 +287,7 @@ class SellingController(StockController):
bold(ref_rate_field),
bold("net rate"),
bold(rate),
bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
get_link_to_form("Selling Settings", "Selling Settings"),
),
title=_("Invalid Selling Price"),
@@ -298,7 +299,6 @@ class SellingController(StockController):
return
is_internal_customer = self.get("is_internal_customer")
valuation_rate_map = {}
for item in self.items:
if not item.item_code or item.is_free_item:
@@ -308,7 +308,9 @@ class SellingController(StockController):
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
)
last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
last_purchase_rate_in_sales_uom = flt(
last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
)
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
@@ -316,50 +318,17 @@ class SellingController(StockController):
if is_internal_customer or not is_stock_item:
continue
valuation_rate_map[(item.item_code, item.warehouse)] = None
if not valuation_rate_map:
return
or_conditions = (
f"""(item_code = {frappe.db.escape(valuation_rate[0])}
and warehouse = {frappe.db.escape(valuation_rate[1])})"""
for valuation_rate in valuation_rate_map
)
valuation_rates = frappe.db.sql(
f"""
select
item_code, warehouse, valuation_rate
from
`tabBin`
where
({" or ".join(or_conditions)})
and valuation_rate > 0
""",
as_dict=True,
)
for rate in valuation_rates:
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
for item in self.items:
if not item.item_code or item.is_free_item:
continue
last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
if not last_valuation_rate:
continue
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
rate_field = "valuation_rate" if self.doctype in ["Sales Order", "Quotation"] else "incoming_rate"
if item.get(rate_field) and item.base_net_rate < (
valuation_rate := flt(
item.get(rate_field) * (item.conversion_factor or 1), item.precision("base_net_rate")
)
):
throw_message(
item.idx,
item.item_name,
last_valuation_rate_in_sales_uom,
"valuation rate (Moving Average)",
valuation_rate,
"valuation rate",
)
def get_item_list(self):
@@ -515,9 +484,35 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self):
def reset_incoming_rate():
old_item = next(
(
item
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
if item.name == d.name
),
None,
)
if old_item:
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
if (
old_item.item_code != d.item_code
or old_item.warehouse != d.warehouse
or old_qty != qty
or old_item.serial_no != d.serial_no
or get_serial_nos(old_item.serial_and_batch_bundle)
!= get_serial_nos(d.serial_and_batch_bundle)
or old_item.batch_no != d.batch_no
or get_batch_nos(old_item.serial_and_batch_bundle)
!= get_batch_nos(d.serial_and_batch_bundle)
):
d.incoming_rate = 0
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
@@ -525,6 +520,9 @@ class SellingController(StockController):
"Selling Settings", "set_zero_rate_for_expired_batch"
)
is_standalone = self.is_return and not self.return_against
old_doc = self.get_doc_before_save()
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
@@ -554,6 +552,9 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
if old_doc:
reset_incoming_rate()
if (
not d.incoming_rate
or self.is_internal_transfer()
@@ -571,11 +572,12 @@ class SellingController(StockController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": d.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
"allow_zero_valuation": d.get("allow_zero_valuation_rate"),
"batch_no": d.batch_no,
"serial_no": d.serial_no,
},
raise_error_if_no_rate=False,
raise_error_if_no_rate=is_standalone,
fallbacks=not is_standalone,
)
if (

View File

@@ -83,7 +83,8 @@ status_map = {
],
"Delivery Note": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
@@ -110,7 +111,7 @@ status_map = {
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
[
"Ordered",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
],
[
"Transferred",
@@ -341,14 +342,17 @@ class StatusUpdater(Document):
):
return
if qty_or_amount == "qty":
action_msg = _(
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
)
if args["target_dt"] != "Quotation Item":
if qty_or_amount == "qty":
action_msg = _(
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
)
else:
action_msg = _(
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
)
else:
action_msg = _(
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
)
action_msg = None
frappe.throw(
_(
@@ -360,8 +364,7 @@ class StatusUpdater(Document):
frappe.bold(_(self.doctype)),
frappe.bold(item.get("item_code")),
)
+ "<br><br>"
+ action_msg,
+ ("<br><br>" + action_msg if action_msg else ""),
OverAllowanceError,
title=_("Limit Crossed"),
)

View File

@@ -57,6 +57,8 @@ class StockController(AccountsController):
if not self.get("is_return"):
self.validate_inspection()
self.validate_warehouse_of_sabb()
self.validate_serialized_batch()
self.clean_serial_nos()
self.validate_customer_provided_item()
@@ -65,6 +67,45 @@ class StockController(AccountsController):
self.validate_putaway_capacity()
self.reset_conversion_factor()
def validate_warehouse_of_sabb(self):
if self.is_internal_transfer():
return
doc_before_save = self.get_doc_before_save()
for row in self.items:
if not row.get("serial_and_batch_bundle"):
continue
sabb_details = frappe.db.get_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,
["type_of_transaction", "warehouse", "has_serial_no"],
as_dict=True,
)
if not sabb_details:
continue
if sabb_details.type_of_transaction != "Outward":
continue
warehouse = row.get("warehouse") or row.get("s_warehouse")
if sabb_details.warehouse != warehouse:
frappe.throw(
_(
"Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}."
).format(row.idx, warehouse, sabb_details.warehouse, row.serial_and_batch_bundle)
)
if self.doctype == "Stock Reconciliation":
continue
if sabb_details.has_serial_no and doc_before_save and doc_before_save.get("items"):
prev_row = doc_before_save.get("items", {"idx": row.idx})
if prev_row and prev_row[0].serial_and_batch_bundle != row.serial_and_batch_bundle:
sabb_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
sabb_doc.validate_serial_no_status()
def reset_conversion_factor(self):
for row in self.get("items"):
if row.uom != row.stock_uom:
@@ -465,7 +506,10 @@ class StockController(AccountsController):
if is_rejected:
serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward"
qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
qty = flt(
row.get("rejected_qty") * row.get("conversion_factor", 1.0),
frappe.get_precision("Serial and Batch Entry", "qty"),
)
warehouse = row.get("rejected_warehouse")
if (
@@ -1091,6 +1135,16 @@ class StockController(AccountsController):
continue
if qi_required: # validate row only if inspection is required on item level
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
self.validate_qi_presence(row)
if self.docstatus == 1:
self.validate_qi_submission(row)
@@ -1098,16 +1152,6 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
@@ -1588,7 +1632,7 @@ def get_gl_entries_for_preview(doctype, docname, fields):
def get_columns(raw_columns, fields):
return [
{"name": d.get("label"), "editable": False, "width": 110}
{"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")}
for d in raw_columns
if not d.get("hidden") and d.get("fieldname") in fields
]
@@ -1658,7 +1702,7 @@ def check_item_quality_inspection(doctype, items):
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
def make_quality_inspections(company, doctype, docname, items):
if isinstance(items, str):
items = json.loads(items)
@@ -1677,6 +1721,7 @@ def make_quality_inspections(doctype, docname, items):
quality_inspection = frappe.get_doc(
{
"company": company,
"doctype": "Quality Inspection",
"inspection_type": "Incoming",
"inspected_by": frappe.session.user,

View File

@@ -254,10 +254,10 @@ class SubcontractingController(StockController):
):
for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
fields=["item_code", "(qty - received_qty) as qty", "parent", "bom"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
def __get_transferred_items(self):
se = frappe.qb.DocType("Stock Entry")
@@ -829,13 +829,17 @@ class SubcontractingController(StockController):
self.__set_serial_nos(item_row, rm_obj)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
key = (
item_row.item_code,
item_row.get(self.subcontract_data.order_field),
item_row.get("bom"),
)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
if self.qty_to_be_received.get(key):
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
@@ -880,7 +884,11 @@ class SubcontractingController(StockController):
if self.qty_to_be_received:
self.qty_to_be_received[
(row.item_code, row.get(self.subcontract_data.order_field))
(
row.item_code,
row.get(self.subcontract_data.order_field),
row.get("bom"),
)
] -= row.qty
def __set_rate_for_serial_and_batch_bundle(self):
@@ -1289,6 +1297,55 @@ def make_rm_stock_entry(
if target_doc and target_doc.get("items"):
target_doc.items = []
def post_process(source_doc, target_doc):
target_doc.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
target_doc.purchase_order = source_doc.name
else:
target_doc.subcontracting_order = source_doc.name
target_doc.set_stock_entry_type()
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item.get("qty")
or max(
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
),
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": source_doc.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
target_doc.add_to_stock_entry_detail(items_dict)
stock_entry = get_mapped_doc(
order_doctype,
subcontract_order.name,
@@ -1309,53 +1366,9 @@ def make_rm_stock_entry(
},
target_doc,
ignore_child_tables=True,
postprocess=post_process,
)
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.set_stock_entry_type()
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item.get("qty")
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
if target_doc:
return stock_entry
else:
@@ -1387,6 +1400,8 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
):
rm_detail_field = "po_detail" if order_doctype == "Purchase Order" else "sco_rm_detail"
def post_process(source_doc, target_doc):
target_doc.purpose = "Material Transfer"
@@ -1397,6 +1412,21 @@ def make_return_stock_entry_for_subcontract(
target_doc.company = source_doc.company
target_doc.is_return = 1
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(target_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(target_doc, value, value.qty, rm_details, rm_detail_field)
target_doc.set_stock_entry_type()
ste_doc = get_mapped_doc(
order_doctype,
@@ -1411,27 +1441,6 @@ def make_return_stock_entry_for_subcontract(
postprocess=post_process,
)
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type()
return ste_doc

View File

@@ -602,6 +602,11 @@ class calculate_taxes_and_totals:
else:
self.grand_total_diff = 0
# Apply rounding adjustment to grand_total_for_distributing_discount
# to prevent precision errors during discount distribution
if hasattr(self, "grand_total_for_distributing_discount") and not self.discount_amount_applied:
self.grand_total_for_distributing_discount += self.grand_total_diff
def calculate_totals(self):
grand_total_diff = self.grand_total_diff

View File

@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
self.assertEqual(so.total, 1500)
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
self.assertEqual(so.grand_total, 1400)
def test_100_percent_discount_with_inclusive_tax(self):
"""Test that 100% discount with inclusive taxes results in zero net_total"""
so = make_sales_order(do_not_save=1)
so.apply_discount_on = "Grand Total"
so.items[0].qty = 2
so.items[0].rate = 1300
so.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Account VAT",
"included_in_print_rate": True,
"rate": 9,
},
)
so.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Account Service Tax",
"included_in_print_rate": True,
"rate": 9,
},
)
so.save()
# Apply 100% discount
so.discount_amount = 2600
calculate_taxes_and_totals(so)
# net_total should be exactly 0, not 0.01
self.assertEqual(so.net_total, 0)
self.assertEqual(so.grand_total, 0)

View File

@@ -38,18 +38,18 @@ class EmailCampaign(Document):
def set_date(self):
if getdate(self.start_date) < getdate(today()):
frappe.throw(_("Start Date cannot be before the current date"))
# set the end date as start date + max(send after days) in campaign schedule
send_after_days = []
campaign = frappe.get_doc("Campaign", self.campaign_name)
for entry in campaign.get("campaign_schedules"):
send_after_days.append(entry.send_after_days)
try:
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
except ValueError:
campaign = frappe.get_cached_doc("Campaign", self.campaign_name)
send_after_days = [entry.send_after_days for entry in campaign.get("campaign_schedules")]
if not send_after_days:
frappe.throw(
_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)
)
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
def validate_lead(self):
lead_email_id = frappe.db.get_value("Lead", self.recipient, "email_id")
if not lead_email_id:
@@ -77,58 +77,128 @@ class EmailCampaign(Document):
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today_date = getdate(today())
if start_date > today_date:
self.db_set("status", "Scheduled", update_modified=False)
new_status = "Scheduled"
elif end_date >= today_date:
self.db_set("status", "In Progress", update_modified=False)
elif end_date < today_date:
self.db_set("status", "Completed", update_modified=False)
new_status = "In Progress"
else:
new_status = "Completed"
if self.status != new_status:
self.db_set("status", new_status, update_modified=False)
# called through hooks to send campaign mails to leads
def send_email_to_leads_or_contacts():
today_date = getdate(today())
# Get all active email campaigns in a single query
email_campaigns = frappe.get_all(
"Email Campaign", filters={"status": ("not in", ["Unsubscribed", "Completed", "Scheduled"])}
"Email Campaign",
filters={"status": "In Progress"},
fields=["name", "campaign_name", "email_campaign_for", "recipient", "start_date", "sender"],
)
for camp in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", camp.name)
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
if not email_campaigns:
return
# Process each email campaign
for email_campaign in email_campaigns:
try:
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
except frappe.DoesNotExistError:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("Campaign {0} not found").format(email_campaign.campaign_name),
)
continue
# Find schedules that match today
for entry in campaign.get("campaign_schedules"):
scheduled_date = add_days(email_campaign.get("start_date"), entry.get("send_after_days"))
if scheduled_date == getdate(today()):
send_mail(entry, email_campaign)
try:
scheduled_date = add_days(getdate(email_campaign.start_date), entry.get("send_after_days"))
if scheduled_date == today_date:
send_mail(entry, email_campaign)
except Exception:
frappe.log_error(
title=_("Email Campaign Send Error"),
message=_("Failed to send email for campaign {0} to {1}").format(
email_campaign.name, email_campaign.recipient
),
)
def send_mail(entry, email_campaign):
recipient_list = []
if email_campaign.email_campaign_for == "Email Group":
for member in frappe.db.get_list(
"Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"]
):
recipient_list.append(member["email"])
campaign_for = email_campaign.get("email_campaign_for")
recipient = email_campaign.get("recipient")
sender_user = email_campaign.get("sender")
campaign_name = email_campaign.get("name")
# Get recipient emails
if campaign_for == "Email Group":
recipient_list = frappe.get_all(
"Email Group Member",
filters={"email_group": recipient, "unsubscribed": 0},
pluck="email",
)
else:
recipient_list.append(
frappe.db.get_value(
email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id"
email_id = frappe.db.get_value(campaign_for, recipient, "email_id")
if not email_id:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("No email found for {0} {1}").format(campaign_for, recipient),
)
return
recipient_list = [email_id]
if not recipient_list:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("No recipients found for campaign {0}").format(campaign_name),
)
return
# Get email template and sender
email_template = frappe.get_cached_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", sender_user, "email") if sender_user else None
# Build context for template rendering
if campaign_for != "Email Group":
context = {"doc": frappe.get_doc(campaign_for, recipient)}
else:
# For email groups, use the email group document as context
context = {"doc": frappe.get_doc("Email Group", recipient)}
# Render template
subject = frappe.render_template(email_template.get("subject"), context)
content = frappe.render_template(email_template.response_, context)
try:
comm = make(
doctype="Email Campaign",
name=campaign_name,
subject=subject,
content=content,
sender=sender,
recipients=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=False,
email_template=email_template.name,
)
email_template = frappe.get_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", email_campaign.get("sender"), "email")
context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)}
# send mail and link communication to document
comm = make(
doctype="Email Campaign",
name=email_campaign.name,
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,
email_template=email_template.name,
)
frappe.sendmail(
recipients=recipient_list,
subject=subject,
content=content,
sender=sender,
communication=comm["name"],
queue_separately=True,
)
except Exception:
frappe.log_error(title="Email Campaign Failed.")
return comm
@@ -140,7 +210,12 @@ def unsubscribe_recipient(unsubscribe, method):
# called through hooks to update email campaign status daily
def set_email_campaign_status():
email_campaigns = frappe.get_all("Email Campaign", filters={"status": ("!=", "Unsubscribed")})
for entry in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", entry.name)
email_campaigns = frappe.get_all(
"Email Campaign",
filters={"status": ("!=", "Unsubscribed")},
pluck="name",
)
for name in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", name)
email_campaign.update_status()

View File

@@ -1,96 +1,48 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:market_segment",
"beta": 0,
"creation": "2018-10-01 09:59:14.479509",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_rename": 1,
"autoname": "field:market_segment",
"creation": "2018-10-01 09:59:14.479509",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"market_segment"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "market_segment",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Market Segment",
"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,
"fieldname": "market_segment",
"fieldtype": "Data",
"label": "Market Segment",
"unique": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-01 09:59:14.479509",
"modified_by": "Administrator",
"module": "CRM",
"name": "Market Segment",
"name_case": "",
"owner": "Administrator",
],
"links": [],
"modified": "2025-12-17 12:09:34.687368",
"modified_by": "Administrator",
"module": "CRM",
"name": "Market Segment",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"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
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"translated_doctype": 1
}

View File

@@ -307,6 +307,21 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
};
});
this.frm.set_query("uom", "items", function (doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (!row.item_code) {
return;
}
return {
query: "erpnext.controllers.queries.get_item_uom_query",
filters: {
item_code: row.item_code,
},
};
});
me.frm.set_query("contact_person", erpnext.queries["contact_query"]);
if (me.frm.doc.opportunity_from == "Lead") {

View File

@@ -59,7 +59,9 @@ def create_prospect_against_crm_deal():
)
pass
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
if doc.contacts and len(doc.contacts):
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
create_address("Prospect", prospect_name, doc.address)
frappe.response["message"] = prospect_name

View File

@@ -256,7 +256,7 @@ standard_portal_menu_items = [
"role": "Customer",
},
{"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"},
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address", "role": "Customer"},
{
"title": "Timesheets",
"route": "/timesheets",

View File

@@ -133,7 +133,7 @@
"label": "Batch Size"
},
{
"depends_on": "eval:doc.parenttype == \"Routing\"",
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
"fieldname": "sequence_id",
"fieldtype": "Int",
"label": "Sequence ID"
@@ -196,7 +196,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-31 16:17:47.287117",
"modified": "2026-02-17 15:33:28.495850",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -1076,9 +1076,9 @@ class JobCard(Document):
def is_work_order_closed(self):
if self.work_order:
status = frappe.get_value("Work Order", self.work_order)
status = frappe.get_value("Work Order", self.work_order, "status")
if status == "Closed":
if status in ["Closed", "Stopped"]:
return True
return False

View File

@@ -49,7 +49,8 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"set_only_once": 1
},
{
"depends_on": "eval:!doc.__islocal && doc.warehouse",
@@ -66,13 +67,14 @@
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
"options": "Company",
"set_only_once": 1
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-19 19:06:36.481625",
"modified": "2026-02-17 11:53:17.940039",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",

View File

@@ -478,7 +478,7 @@ class ProductionPlan(Document):
item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
bom_no = item_details.bom_no
bom_no = item_details.get("bom_no")
if data.get("bom_no"):
bom_no = data.get("bom_no")
@@ -648,8 +648,8 @@ class ProductionPlan(Document):
self.status = "Completed"
if self.status != "Completed":
self.update_ordered_status()
self.update_requested_status()
self.update_ordered_status()
if close is not None:
self.db_set("status", self.status)
@@ -658,25 +658,17 @@ class ProductionPlan(Document):
self.update_bin_qty()
def update_ordered_status(self):
update_status = False
for d in self.po_items:
if d.planned_qty == d.ordered_qty:
update_status = True
if update_status and self.status != "Completed":
self.status = "In Process"
for child_table in ["po_items", "sub_assembly_items"]:
for item in self.get(child_table):
if item.ordered_qty:
self.status = "In Process"
return
def update_requested_status(self):
if not self.mr_items:
return
update_status = True
for d in self.mr_items:
if d.quantity != d.requested_qty:
update_status = False
if update_status:
self.status = "Material Requested"
if d.requested_qty:
self.status = "Material Requested"
break
def get_production_items(self):
item_dict = {}
@@ -701,19 +693,21 @@ class ProductionPlan(Document):
"project": self.project,
}
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse, d.planned_start_date)
if self.combine_items:
key = (d.item_code, d.sales_order, d.warehouse)
key = (d.item_code, d.sales_order, d.warehouse, d.planned_start_date)
if not d.sales_order:
key = (d.name, d.item_code, d.warehouse)
key = (d.name, d.item_code, d.warehouse, d.planned_start_date)
if not item_details["project"] and d.sales_order:
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
if self.get_items_from == "Material Request":
item_details.update({"qty": d.planned_qty})
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
item_dict[
(d.item_code, d.material_request_item, d.warehouse, d.planned_start_date)
] = item_details
else:
item_details.update(
{
@@ -796,6 +790,8 @@ class ProductionPlan(Document):
"stock_uom",
"bom_level",
"schedule_date",
"sales_order",
"sales_order_item",
]:
if row.get(field):
wo_data[field] = row.get(field)
@@ -835,6 +831,8 @@ class ProductionPlan(Document):
"qty",
"description",
"production_plan_item",
"sales_order",
"sales_order_item",
]:
po_data[field] = row.get(field)
@@ -1021,6 +1019,10 @@ class ProductionPlan(Document):
if not is_group_warehouse:
data.fg_warehouse = self.sub_assembly_warehouse
if not self.combine_sub_items:
data.sales_order = row.sales_order
data.sales_order_item = row.sales_order_item
def set_default_supplier_for_subcontracting_order(self):
items = [
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"

View File

@@ -565,6 +565,90 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(po_doc.items[0].fg_item, fg_item)
self.assertEqual(po_doc.items[0].item_code, service_item)
def test_sales_order_references_for_sub_assembly_items(self):
"""
Test that Sales Order and Sales Order Item references in Work Order and Purchase Order
are correctly propagated from the Production Plan.
"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
# Setup Test Items & BOM
fg_item = "Test FG Good Item"
sub_assembly_item1 = "Test Sub Assembly Item 1"
sub_assembly_item2 = "Test Sub Assembly Item 2"
bom_tree = {
fg_item: {
sub_assembly_item1: {"Test Raw Material 1": {}},
sub_assembly_item2: {"Test Raw Material 2": {}},
}
}
create_nested_bom(bom_tree, prefix="")
# Create Sales Order
so = make_sales_order(item_code=fg_item, qty=10)
so_item_row = so.items[0].name
# Create Production Plan from Sales Order
production_plan = frappe.new_doc("Production Plan")
production_plan.company = so.company
production_plan.get_items_from = "Sales Order"
production_plan.item_code = fg_item
production_plan.get_open_sales_orders()
self.assertEqual(production_plan.sales_orders[0].sales_order, so.name)
production_plan.get_so_items()
production_plan.skip_available_sub_assembly_item = 0
production_plan.get_sub_assembly_items()
self.assertEqual(len(production_plan.sub_assembly_items), 2)
# Validate Sales Order references in Sub Assembly Items
for row in production_plan.sub_assembly_items:
if row.production_item == sub_assembly_item1:
row.supplier = "_Test Supplier"
row.type_of_manufacturing = "Subcontract"
self.assertEqual(row.sales_order, so.name)
self.assertEqual(row.sales_order_item, so_item_row)
# Submit Production Plan
production_plan.save()
production_plan.submit()
production_plan.make_work_order()
# Validate Purchase Order (Subcontracted Item)
po_items = frappe.get_all(
"Purchase Order Item",
{
"production_plan": production_plan.name,
"fg_item": sub_assembly_item1,
},
["sales_order", "sales_order_item"],
)
self.assertTrue(po_items)
self.assertEqual(po_items[0].sales_order, so.name)
self.assertEqual(po_items[0].sales_order_item, so_item_row)
# Validate Work Order (In-house Item)
work_orders = frappe.get_all(
"Work Order",
{
"production_plan": production_plan.name,
"production_item": sub_assembly_item2,
},
["sales_order", "sales_order_item"],
)
self.assertTrue(work_orders)
self.assertEqual(work_orders[0].sales_order, so.name)
self.assertEqual(work_orders[0].sales_order_item, so_item_row)
def test_production_plan_combine_subassembly(self):
"""
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
@@ -867,7 +951,7 @@ class TestProductionPlan(FrappeTestCase):
items_data = pln.get_production_items()
# Update qty
items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
items_data[(pln.po_items[0].name, item, None, pln.po_items[0].planned_start_date)]["qty"] = qty
# Create and Submit Work Order for each item in items_data
for _key, item in items_data.items():

View File

@@ -10,6 +10,8 @@
"fg_warehouse",
"parent_item_code",
"schedule_date",
"sales_order",
"sales_order_item",
"column_break_3",
"qty",
"bom_no",
@@ -212,20 +214,36 @@
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order",
"read_only": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Sales Order Item",
"no_copy": 1,
"print_hide": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-06-10 13:36:24.759101",
"modified": "2026-02-17 12:06:02.309032",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -33,6 +33,8 @@ class ProductionPlanSubAssemblyItem(Document):
purchase_order: DF.Link | None
qty: DF.Float
received_qty: DF.Float
sales_order: DF.Link | None
sales_order_item: DF.Data | None
schedule_date: DF.Datetime | None
stock_uom: DF.Link | None
supplier: DF.Link | None

View File

@@ -56,7 +56,6 @@ class TestRouting(FrappeTestCase):
self.assertEqual(job_card_doc.total_completed_qty, 10)
wo_doc.cancel()
wo_doc.delete()
def test_update_bom_operation_time(self):
"""Update cost shouldn't update routing times."""

View File

@@ -595,6 +595,33 @@ class TestWorkOrder(FrappeTestCase):
work_order1.cancel()
work_order.cancel()
def test_planned_qty_updates_after_closing_work_order(self):
item_code = "_Test FG Item"
fg_warehouse = "_Test Warehouse 1 - _TC"
planned_before = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
wo = make_wo_order_test_record(item=item_code, fg_warehouse=fg_warehouse, qty=10)
planned_after_submit = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
self.assertEqual(planned_after_submit, planned_before + 10)
close_work_order(wo.name, "Closed")
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "status"), "Closed")
planned_after_close = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
self.assertEqual(planned_after_close, planned_before)
def test_work_order_with_non_transfer_item(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")

View File

@@ -425,10 +425,11 @@ frappe.ui.form.on("Work Order", {
var added_min = false;
// produced qty
var title = __("{0} items produced", [frm.doc.produced_qty]);
let produced_qty = frm.doc.produced_qty - frm.doc.disassembled_qty;
var title = __("{0} items produced", [produced_qty]);
bars.push({
title: title,
width: (frm.doc.produced_qty / frm.doc.qty) * 100 + "%",
width: (flt(produced_qty) / frm.doc.qty) * 100 + "%",
progress_class: "progress-bar-success",
});
if (bars[0].width == "0%") {
@@ -445,14 +446,27 @@ frappe.ui.form.on("Work Order", {
if (pending_complete > 0) {
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
title = __("{0} items in progress", [pending_complete]);
let progress_class = "progress-bar-warning";
if (frm.doc.status == "Closed") {
if (frm.doc.required_items.find((d) => d.returned_qty > 0)) {
title = __("{0} items returned", [pending_complete]);
progress_class = "progress-bar-warning";
} else {
title = __("{0} items to return", [pending_complete]);
progress_class = "progress-bar-info";
}
}
bars.push({
title: title,
width: (width > 100 ? "99.5" : width) + "%",
progress_class: "progress-bar-warning",
progress_class: progress_class,
});
message = message + ". " + title;
}
}
//process loss qty
if (frm.doc.process_loss_qty) {
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
@@ -463,6 +477,19 @@ frappe.ui.form.on("Work Order", {
});
message = message + ". " + title;
}
// disassembled qty
if (frm.doc.disassembled_qty) {
var disassembled_width = (frm.doc.disassembled_qty / frm.doc.qty) * 100;
title = __("{0} items disassembled", [frm.doc.disassembled_qty]);
bars.push({
title: title,
width: disassembled_width + "%",
progress_class: "progress-bar-secondary",
});
message = message + ". " + title;
}
frm.dashboard.add_progress(__("Status"), bars, message);
},
@@ -664,7 +691,7 @@ erpnext.work_order = {
set_custom_buttons: function (frm) {
var doc = frm.doc;
if (doc.docstatus === 1 && doc.status !== "Closed") {
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
frm.add_custom_button(
__("Close"),
function () {
@@ -674,9 +701,6 @@ erpnext.work_order = {
},
__("Status")
);
}
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
if (doc.status != "Stopped" && doc.status != "Completed") {
frm.add_custom_button(
__("Stop"),

View File

@@ -220,39 +220,52 @@ class WorkOrder(Document):
)
def validate_sales_order(self):
if self.production_plan_sub_assembly_item:
return
if self.sales_order:
self.check_sales_order_on_hold_or_close()
so = frappe.db.sql(
"""
select so.name, so_item.delivery_date, so.project
from `tabSales Order` so
inner join `tabSales Order Item` so_item on so_item.parent = so.name
left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent
where so.name=%s and so.docstatus = 1
and so.skip_delivery_note = 0 and (
so_item.item_code=%s or
pk_item.item_code=%s )
""",
(self.sales_order, self.production_item, self.production_item),
as_dict=1,
SalesOrder = frappe.qb.DocType("Sales Order")
SalesOrderItem = frappe.qb.DocType("Sales Order Item")
PackedItem = frappe.qb.DocType("Packed Item")
ProductBundleItem = frappe.qb.DocType("Product Bundle Item")
so = (
frappe.qb.from_(SalesOrder)
.inner_join(SalesOrderItem)
.on(SalesOrderItem.parent == SalesOrder.name)
.left_join(ProductBundleItem)
.on(ProductBundleItem.parent == SalesOrderItem.item_code)
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
.where(
(SalesOrder.skip_delivery_note == 0)
& (SalesOrder.docstatus == 1)
& (SalesOrder.name == self.sales_order)
& (
(SalesOrderItem.item_code == self.production_item)
| (ProductBundleItem.item_code == self.production_item)
)
)
.run(as_dict=1)
)
if not so:
so = frappe.db.sql(
"""
select
so.name, so_item.delivery_date, so.project
from
`tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item
where so.name=%s
and so.name=so_item.parent
and so.name=packed_item.parent
and so.skip_delivery_note = 0
and so_item.item_code = packed_item.parent_item
and so.docstatus = 1 and packed_item.item_code=%s
""",
(self.sales_order, self.production_item),
as_dict=1,
so = (
frappe.qb.from_(SalesOrder)
.inner_join(SalesOrderItem)
.on(SalesOrderItem.parent == SalesOrder.name)
.inner_join(PackedItem)
.on(PackedItem.parent == SalesOrder.name)
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
.where(
(SalesOrder.name == self.sales_order)
& (SalesOrder.skip_delivery_note == 0)
& (SalesOrderItem.item_code == PackedItem.parent_item)
& (SalesOrder.docstatus == 1)
& (PackedItem.item_code == self.production_item)
)
.run(as_dict=1)
)
if len(so):
@@ -358,7 +371,7 @@ class WorkOrder(Document):
if status != self.status:
self.db_set("status", status)
self.update_required_items()
self.update_required_items()
return status or self.status
@@ -426,7 +439,7 @@ class WorkOrder(Document):
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
if self.sales_order and self.sales_order_item:
if self.sales_order and self.sales_order_item and not self.production_plan_sub_assembly_item:
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan:
@@ -517,7 +530,6 @@ class WorkOrder(Document):
self.db_set("status", "Cancelled")
self.on_close_or_cancel()
self.delete_job_card()
def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists(
@@ -531,7 +543,6 @@ class WorkOrder(Document):
self.update_planned_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
self.delete_auto_created_batch_and_serial_no()
def create_serial_no_batch_no(self):
if not (self.has_serial_no or self.has_batch_no):
@@ -588,13 +599,6 @@ class WorkOrder(Document):
)
)
def delete_auto_created_batch_and_serial_no(self):
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
frappe.delete_doc("Serial No", row.name)
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args):
item_details = frappe.get_cached_value(
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
@@ -827,7 +831,7 @@ class WorkOrder(Document):
doc.db_set("status", doc.status)
def update_work_order_qty_in_so(self):
if not self.sales_order and not self.sales_order_item:
if (not self.sales_order and not self.sales_order_item) or self.production_plan_sub_assembly_item:
return
total_bundle_qty = 1
@@ -1027,10 +1031,6 @@ class WorkOrder(Document):
if self.actual_start_date and self.actual_end_date:
self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60)
def delete_job_card(self):
for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}):
frappe.delete_doc("Job Card", d.name)
def validate_production_item(self):
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
@@ -1173,6 +1173,7 @@ class WorkOrder(Document):
"operation": item.operation or operation,
"item_code": item.item_code,
"item_name": item.item_name,
"stock_uom": item.stock_uom,
"description": item.description,
"allow_alternative_item": item.allow_alternative_item,
"required_qty": item.qty,
@@ -1197,7 +1198,7 @@ class WorkOrder(Document):
.select(
ste_child.item_code,
ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"),
fn.Sum(ste_child.transfer_qty).as_("qty"),
)
.where(
(ste.docstatus == 1)
@@ -1227,7 +1228,7 @@ class WorkOrder(Document):
.select(
ste_child.item_code,
ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"),
fn.Sum(ste_child.transfer_qty).as_("qty"),
)
.where(
(ste.docstatus == 1)
@@ -1607,8 +1608,8 @@ def close_work_order(work_order, status):
)
)
work_order.on_close_or_cancel()
work_order.update_status(status)
work_order.on_close_or_cancel()
frappe.msgprint(_("Work Order has been {0}").format(status))
work_order.notify_update()
return work_order.status
@@ -1765,6 +1766,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
target_doc,
)
doc.purpose = "Material Transfer for Manufacture"
doc.for_qty = for_qty
doc.set_item_locations()

View File

@@ -6,129 +6,111 @@ import frappe
from frappe import _, scrub
from frappe.utils import getdate, today
from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges
from erpnext.stock.report.stock_analytics.stock_analytics import (
get_period,
get_period_columns,
get_period_date_ranges,
)
WORK_ORDER_STATUS_LIST = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
def execute(filters=None):
columns = get_columns(filters)
data, chart = get_data(filters, columns)
period_columns = get_period_columns(filters)
columns = get_columns(period_columns)
data, chart = get_data(filters, period_columns)
return columns, data, None, chart
def get_columns(filters):
columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}]
ranges = get_period_date_ranges(filters)
for _dummy, end_date in ranges:
period = get_period(end_date, filters)
columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120})
def get_columns(period_columns):
columns = [{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 140}]
columns.extend(period_columns)
return columns
def get_periodic_data(filters, entry):
periodic_data = {
"Not Started": {},
"Overdue": {},
"Pending": {},
"Completed": {},
"Closed": {},
"Stopped": {},
}
def get_work_orders(filters):
from_date = filters.get("from_date")
to_date = filters.get("to_date")
ranges = get_period_date_ranges(filters)
WorkOrder = frappe.qb.DocType("Work Order")
for from_date, end_date in ranges:
period = get_period(end_date, filters)
for d in entry:
if getdate(from_date) <= getdate(d.creation) <= getdate(end_date) and d.status not in [
"Draft",
"Submitted",
"Completed",
"Cancelled",
]:
if d.status in ["Not Started", "Closed", "Stopped"]:
periodic_data = update_periodic_data(periodic_data, d.status, period)
elif getdate(today()) > getdate(d.planned_end_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
elif getdate(today()) < getdate(d.planned_end_date):
periodic_data = update_periodic_data(periodic_data, "Pending", period)
if (
getdate(from_date) <= getdate(d.actual_end_date) <= getdate(end_date)
and d.status == "Completed"
):
periodic_data = update_periodic_data(periodic_data, "Completed", period)
return periodic_data
def update_periodic_data(periodic_data, status, period):
if periodic_data.get(status).get(period):
periodic_data[status][period] += 1
else:
periodic_data[status][period] = 1
return periodic_data
def get_data(filters, columns):
data = []
entry = frappe.get_all(
"Work Order",
fields=[
"creation",
"actual_end_date",
"planned_end_date",
"status",
],
filters={"docstatus": 1, "company": filters["company"]},
return (
frappe.qb.from_(WorkOrder)
.select(WorkOrder.creation, WorkOrder.actual_end_date, WorkOrder.planned_end_date, WorkOrder.status)
.where(
(WorkOrder.docstatus == 1)
& (WorkOrder.company == filters.get("company"))
& (
(WorkOrder.creation.between(from_date, to_date))
| (WorkOrder.actual_end_date.between(from_date, to_date))
)
)
.run(as_dict=True)
)
periodic_data = get_periodic_data(filters, entry)
labels = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
chart_data = get_chart_data(periodic_data, columns)
ranges = get_period_date_ranges(filters)
def get_data(filters, period_columns):
ranges = build_ranges(filters)
period_labels = [scrub(pd) for _fd, _td, pd in ranges]
periodic_data = {status: {pd: 0 for pd in period_labels} for status in WORK_ORDER_STATUS_LIST}
entries = get_work_orders(filters)
for label in labels:
work = {}
work["Status"] = _(label)
for _dummy, end_date in ranges:
period = get_period(end_date, filters)
if periodic_data.get(label).get(period):
work[scrub(period)] = periodic_data.get(label).get(period)
for d in entries:
if d.status == "Completed":
if not d.actual_end_date:
continue
if period := scrub(get_period_for_date(getdate(d.actual_end_date), ranges)):
periodic_data["Completed"][period] += 1
continue
creation_date = getdate(d.creation)
period = scrub(get_period_for_date(creation_date, ranges))
if not period:
continue
if d.status in ("Not Started", "Closed", "Stopped"):
periodic_data[d.status][period] += 1
else:
if d.planned_end_date and getdate(today()) > getdate(d.planned_end_date):
periodic_data["Overdue"][period] += 1
else:
work[scrub(period)] = 0.0
data.append(work)
periodic_data["Pending"][period] += 1
return data, chart_data
data = []
for status in WORK_ORDER_STATUS_LIST:
row = {"status": _(status)}
for _fd, _td, period in ranges:
row[scrub(period)] = periodic_data[status].get(scrub(period), 0)
data.append(row)
chart = get_chart_data(periodic_data, period_columns)
return data, chart
def get_chart_data(periodic_data, columns):
labels = [d.get("label") for d in columns[1:]]
def get_period_for_date(date, ranges):
for from_date, to_date, period in ranges:
if from_date <= date <= to_date:
return period
return None
def build_ranges(filters):
ranges = []
for from_date, end_date in get_period_date_ranges(filters):
period = get_period(end_date, filters)
ranges.append((getdate(from_date), getdate(end_date), period))
return ranges
def get_chart_data(periodic_data, period_columns):
period_labels = [col.get("label") for col in period_columns]
period_fieldnames = [col.get("fieldname") for col in period_columns]
not_start, overdue, pending, completed, closed, stopped = [], [], [], [], [], []
datasets = []
for status in WORK_ORDER_STATUS_LIST:
values = [periodic_data.get(status, {}).get(fieldname, 0) for fieldname in period_fieldnames]
datasets.append({"name": _(status), "values": values})
for d in labels:
not_start.append(periodic_data.get("Not Started").get(d))
overdue.append(periodic_data.get("Overdue").get(d))
pending.append(periodic_data.get("Pending").get(d))
completed.append(periodic_data.get("Completed").get(d))
closed.append(periodic_data.get("Closed").get(d))
stopped.append(periodic_data.get("Stopped").get(d))
datasets.append({"name": _("Not Started"), "values": not_start})
datasets.append({"name": _("Overdue"), "values": overdue})
datasets.append({"name": _("Pending"), "values": pending})
datasets.append({"name": _("Completed"), "values": completed})
datasets.append({"name": _("Closed"), "values": closed})
datasets.append({"name": _("Stopped"), "values": stopped})
chart = {"data": {"labels": labels, "datasets": datasets}}
chart["type"] = "line"
return chart
return {"data": {"labels": period_labels, "datasets": datasets}, "type": "line"}

View File

@@ -75,7 +75,6 @@ erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.move_item_tax_to_item_tax_template
erpnext.patches.v11_1.set_variant_based_on
erpnext.patches.v11_1.woocommerce_set_creation_user
erpnext.patches.v11_1.rename_depends_on_lwp
execute:frappe.delete_doc("Report", "Inactive Items")
erpnext.patches.v11_1.delete_scheduling_tool
erpnext.patches.v12_0.rename_tolerance_fields
@@ -406,6 +405,7 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.add_bank_transaction_as_journal_entry_reference
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v14_0.update_full_name_in_contract
@@ -428,3 +428,7 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po

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