Compare commits

...

276 Commits

Author SHA1 Message Date
Frappe PR Bot
a2626ed55f chore(release): Bumped to Version 15.104.0
# [15.104.0](https://github.com/frappe/erpnext/compare/v15.103.1...v15.104.0) (2026-04-07)

### Bug Fixes

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

### Features

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

### Reverts

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

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

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

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

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

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

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

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

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

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

(cherry picked from commit 66780543bd)

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

* chore: resolve conflicts

* chore: resolve conflicts

---------

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

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

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

### Bug Fixes

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

### Reverts

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

(cherry picked from commit 0696bd2082)

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

* chore: resolve conflicts

---------

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

### Bug Fixes

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

### Features

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

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

(cherry picked from commit fa5238ba12)

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

* chore: resolve conflict

---------

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

(cherry picked from commit ddeb9775ed)

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

* chore: resolve conflicts

---------

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

(cherry picked from commit f3a794384a)

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

* chore: resolve conflicts

---------

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

(cherry picked from commit 6badf00313)

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

* chore: resolve conflicts

* chore: resole conflicts

---------

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

(cherry picked from commit 3f74733942)

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

* chore: resolve conflicts

---------

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

(cherry picked from commit e136bfbb61)

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

* chore: resolve conflicts

---------

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

### Bug Fixes

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

### Features

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

(cherry picked from commit 5154102468)

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

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

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

(cherry picked from commit fa35fbdb8e)

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

* chore: resolve conflicts

---------

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

(cherry picked from commit 8e17c722fb)

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

* chore: resolve conflicts

---------

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

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

### Bug Fixes

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

# Conflicts:
#	erpnext/portal/utils.py
2026-03-19 06:34:50 +00:00
mergify[bot]
af86fd3cb4 fix: consider returned qty in subcontracting report (backport #53616) (#53620)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consider returned qty in subcontracting report (#53616)
2026-03-19 11:52:44 +05:30
mergify[bot]
5a3bc27e2c fix: python error in manufacture entry if transfer against is job card (backport #53615) (#53617)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: python error in manufacture entry if transfer against is job card (#53615)
2026-03-19 05:13:36 +00:00
mergify[bot]
562f93e75c fix: ignore cost center (backport #53063) (#53613)
Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com>
fix: ignore cost center (#53063)
2026-03-19 04:41:10 +00:00
mergify[bot]
e0f1e757f3 fix: check posting_date in args (backport #53303) (#53611)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix: check posting_date in args (#53303)
2026-03-19 04:38:20 +00:00
Frappe PR Bot
572d8530b6 chore(release): Bumped to Version 15.101.2
## [15.101.2](https://github.com/frappe/erpnext/compare/v15.101.1...v15.101.2) (2026-03-18)

### Bug Fixes

* incorrect sle calculation when doc has project ([#53599](https://github.com/frappe/erpnext/issues/53599)) ([9e10dec](9e10dec903))
2026-03-18 13:42:20 +00:00
rohitwaghchaure
0fa8cc76f5 Merge pull request #53602 from frappe/mergify/bp/version-15/pr-53600
fix: incorrect sle calculation when doc has project (backport #53599) (backport #53600)
2026-03-18 19:10:44 +05:30
Mihir Kandoi
9e10dec903 fix: incorrect sle calculation when doc has project (#53599)
(cherry picked from commit 6cb6a52ded)
(cherry picked from commit 7acd435835)
2026-03-18 13:38:17 +00:00
rohitwaghchaure
7b64f88734 Merge pull request #53600 from frappe/mergify/bp/version-15-hotfix/pr-53599
fix: incorrect sle calculation when doc has project (backport #53599)
2026-03-18 19:07:26 +05:30
Mihir Kandoi
7acd435835 fix: incorrect sle calculation when doc has project (#53599)
(cherry picked from commit 6cb6a52ded)
2026-03-18 13:20:17 +00:00
rohitwaghchaure
16fe458b92 Merge pull request #53585 from frappe/mergify/bp/version-15-hotfix/pr-53246
feat: add cost center field to the stock entry accounting dimension tab (backport #53246)
2026-03-18 16:52:27 +05:30
rohitwaghchaure
4c2dba98da chore: fix conflicts
Removed several fields related to additional transfer entries and subcontracting inward orders from the stock entry JSON.
2026-03-18 16:32:56 +05:30
sudarshan-g
e17b5dfe61 feat: add cost center field to the stock entry accounting dimension tab
(cherry picked from commit 47772f4e77)

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

(cherry picked from commit 31d14df37b)

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

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

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

(cherry picked from commit b433852f8a)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-18 04:59:04 +00:00
Frappe PR Bot
c912df95cb chore(release): Bumped to Version 15.101.1
## [15.101.1](https://github.com/frappe/erpnext/compare/v15.101.0...v15.101.1) (2026-03-18)

### Bug Fixes

* add item_name to quick entry fields in Item doctype (backport [#53530](https://github.com/frappe/erpnext/issues/53530)) ([#53532](https://github.com/frappe/erpnext/issues/53532)) ([0e770c0](0e770c0bbd))
* Append existing ignored doctypes in Journal Entry on_cancel instead of overwriting ([b73d970](b73d9700d0))
* **banking:** include paid purchase invoices in reports and bank clearance ([#52675](https://github.com/frappe/erpnext/issues/52675)) ([ab9d960](ab9d960aa8))
* broke cost center filter in get outstanding reference docs ([53e3bfb](53e3bfbf22))
* change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (backport [#53503](https://github.com/frappe/erpnext/issues/53503)) ([#53516](https://github.com/frappe/erpnext/issues/53516)) ([eec8cf8](eec8cf8a71))
* coderebbit review ([05d614e](05d614eb04))
* correct function syntax in TDS Computation Report ([94972da](94972da845))
* correct overlap detection in JobCard.has_overlap (backport [#53473](https://github.com/frappe/erpnext/issues/53473)) ([#53522](https://github.com/frappe/erpnext/issues/53522)) ([d262a65](d262a65b00))
* correct payment terms fetching and recalculation logic ([79b0482](79b04826d9))
* correct payment terms fetching and recalculation logic ([3148816](3148816451))
* Creating new item price incase of changes in expired item price (backport [#53534](https://github.com/frappe/erpnext/issues/53534)) ([#53544](https://github.com/frappe/erpnext/issues/53544)) ([526ffc1](526ffc1176))
* **delivery note:** avoid maintaining si_detail on return delivery note (backport [#52456](https://github.com/frappe/erpnext/issues/52456)) ([#53352](https://github.com/frappe/erpnext/issues/53352)) ([034d460](034d460ae1))
* do not modify rate in the child item merely for comparison (backport [#53301](https://github.com/frappe/erpnext/issues/53301)) ([#53375](https://github.com/frappe/erpnext/issues/53375)) ([0e00ab8](0e00ab8865))
* do not set valuation rate for invoice without update stock ([284ccd1](284ccd1def))
* enhance sorting and optimize GL entry retrieval ([93ebec9](93ebec90ef))
* **italy:** fix e-invoice ScontoMaggiorazione structure and included_in_print_rate support ([#53334](https://github.com/frappe/erpnext/issues/53334)) ([b9c8e8d](b9c8e8d478))
* **manufacturing:** update working hours validation (backport [#53559](https://github.com/frappe/erpnext/issues/53559)) ([#53566](https://github.com/frappe/erpnext/issues/53566)) ([9771ed4](9771ed4c57))
* **minor:** filter bank accounts in bank statement import ([#53481](https://github.com/frappe/erpnext/issues/53481)) ([a5d1afe](a5d1afe304))
* NoneType error when template description is to be copied to variant (backport [#53358](https://github.com/frappe/erpnext/issues/53358)) ([#53365](https://github.com/frappe/erpnext/issues/53365)) ([0612f1c](0612f1c941))
* **p&l_statement:** disable accumulated value filter by default (backport [#53488](https://github.com/frappe/erpnext/issues/53488)) ([#53489](https://github.com/frappe/erpnext/issues/53489)) ([b63b532](b63b5320f2))
* precision issue in production plan (backport [#53370](https://github.com/frappe/erpnext/issues/53370)) ([#53373](https://github.com/frappe/erpnext/issues/53373)) ([5737d2a](5737d2afa3))
* re-calculate taxes and totals after resetting bundle item rate (backport [#53342](https://github.com/frappe/erpnext/issues/53342)) ([#53349](https://github.com/frappe/erpnext/issues/53349)) ([db251c6](db251c6e11))
* refactor GL entry mapping to include voucher type ([c2e6759](c2e67599f5))
* **regional:** rename duplicate Customer fields in Italy setup (backport [#50921](https://github.com/frappe/erpnext/issues/50921)) ([#53397](https://github.com/frappe/erpnext/issues/53397)) ([2a70203](2a70203cab))
* remove redundant pos print format ([#53348](https://github.com/frappe/erpnext/issues/53348)) ([8497d1f](8497d1f8cf))
* sales order indicator should be based on available qty rather th… (backport [#53456](https://github.com/frappe/erpnext/issues/53456)) ([#53457](https://github.com/frappe/erpnext/issues/53457)) ([a6cf31e](a6cf31edad))
* **sales_invoice:** reset payment methods on `pos_profile` change (backport [#53514](https://github.com/frappe/erpnext/issues/53514)) ([#53560](https://github.com/frappe/erpnext/issues/53560)) ([239728e](239728e4d9))
* **serial_and_batch_bundle_selector:** handle CSV attachment properly (backport [#53460](https://github.com/frappe/erpnext/issues/53460)) ([#53461](https://github.com/frappe/erpnext/issues/53461)) ([7a7c4a0](7a7c4a03f0))
* skip validate_stock_accounts when perpetual inventory is disabled ([3bc9190](3bc9190795))
* stock adjustment entry ([ac6c06d](ac6c06daf9))
* **stock:** fix the property setter (backport [#53422](https://github.com/frappe/erpnext/issues/53422)) ([#53573](https://github.com/frappe/erpnext/issues/53573)) ([57815a0](57815a07ac))
* **support-settings:** disable the auto-close tickets feature if `close_issue_after_days` is set to 0 (backport [#53499](https://github.com/frappe/erpnext/issues/53499)) ([#53504](https://github.com/frappe/erpnext/issues/53504)) ([30fe711](30fe711c44))
* **tds-report:** correct party type filtering and refactor ([e5eb540](e5eb5406da))
* test case ([c384564](c384564314))
* update delivery date in line items ([#53331](https://github.com/frappe/erpnext/issues/53331)) ([85c4cc3](85c4cc3e1b))
* update item description in Production Plan Assembly Items table ([e3e9d7b](e3e9d7b19e))
* update label on company change ([908e185](908e185cfe))
* update user status depends on employee status ([c5796fe](c5796fed4a))
* use completion_date not posting date ([6d47660](6d476604df))
* use qb to prevent incorrect sql due to user permissions ([03f0922](03f09222cf))
* valuation rate for no Use Batch wise Valuation batches ([ca6872c](ca6872c768))
2026-03-18 04:58:29 +00:00
diptanilsaha
915315ef1b Merge pull request #53541 from frappe/version-15-hotfix 2026-03-18 10:26:58 +05:30
mergify[bot]
526dc68c72 chore: add documentation link in valuation method field (backport #53564) (#53570)
* chore: add documentation link in valuation method field (#53564)

(cherry picked from commit f319857939)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-17 16:50:12 +00:00
mergify[bot]
9771ed4c57 fix(manufacturing): update working hours validation (backport #53559) (#53566)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(manufacturing): update working hours validation (#53559)
2026-03-17 22:09:00 +05:30
mergify[bot]
57815a07ac fix(stock): fix the property setter (backport #53422) (#53573)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): fix the property setter (#53422)
2026-03-17 22:07:46 +05:30
Arturo
b9c8e8d478 fix(italy): fix e-invoice ScontoMaggiorazione structure and included_in_print_rate support (#53334) 2026-03-17 15:18:14 +00:00
mergify[bot]
526ffc1176 fix: Creating new item price incase of changes in expired item price (backport #53534) (#53544)
Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
fix: Creating new item price incase of changes in expired item price (#53534)
2026-03-17 14:51:13 +00:00
mergify[bot]
239728e4d9 fix(sales_invoice): reset payment methods on pos_profile change (backport #53514) (#53560)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(sales_invoice): reset payment methods on `pos_profile` change (#53514)
2026-03-17 20:17:32 +05:30
rohitwaghchaure
48d211f8a0 Merge pull request #53554 from saeedkola/fix/validate-stock-accounts-perpetual-v15-hotfix
fix: skip validate_stock_accounts in Journal Entry when perpetual inventory is disabled
2026-03-17 18:30:34 +05:30
ruthra kumar
761caba8e8 Merge pull request #53549 from frappe/mergify/bp/version-15-hotfix/pr-53548
fix: incorrect user perms in queries (backport #53548)
2026-03-17 17:14:04 +05:30
Saeed Kola
3bc9190795 fix: skip validate_stock_accounts when perpetual inventory is disabled
When perpetual inventory is disabled, stock transactions produce no GL
entries, so blocking manual Journal Entries against stock accounts is
incorrect. Added an early return guard in validate_stock_accounts()
to skip the check when is_perpetual_inventory_enabled() returns False.
2026-03-17 17:08:29 +05:30
ruthra kumar
3720a8d5c9 Merge pull request #53528 from frappe/mergify/bp/version-15-hotfix/pr-52675
fix(banking): include paid purchase invoices in reports and bank clearance (backport #52675)
2026-03-17 17:01:33 +05:30
ruthra kumar
03f09222cf fix: use qb to prevent incorrect sql due to user permissions
(cherry picked from commit 04b967bd6d)

# Conflicts:
#	erpnext/controllers/queries.py
2026-03-17 16:45:29 +05:30
ruthra kumar
f232024fa4 chore: remove incorrect import
(cherry picked from commit fc2edfbded)

# Conflicts:
#	erpnext/controllers/queries.py
2026-03-17 11:02:32 +00:00
rohitwaghchaure
fd336e8d4b Merge pull request #53537 from frappe/mergify/bp/version-15-hotfix/pr-53500
fix: valuation rate for no Use Batch wise Valuation batches (backport #53500)
2026-03-17 15:53:29 +05:30
rohitwaghchaure
c384564314 fix: test case
Removed company parameter from get_valuation_method call.
2026-03-17 15:05:46 +05:30
Rohit Waghchaure
ca6872c768 fix: valuation rate for no Use Batch wise Valuation batches
(cherry picked from commit 4befa15198)
2026-03-17 08:42:59 +00:00
Sakthivel Murugan S
a5d1afe304 fix(minor): filter bank accounts in bank statement import (#53481) 2026-03-17 11:31:22 +05:30
Nikhil Kothari
a85aeb2f9b chore: resolve conflicts 2026-03-17 11:28:37 +05:30
rohitwaghchaure
158e290580 Merge pull request #53518 from frappe/mergify/bp/version-15-hotfix/pr-53513
fix: do not set valuation rate for invoice without update stock (backport #53513)
2026-03-17 11:23:55 +05:30
mergify[bot]
0e770c0bbd fix: add item_name to quick entry fields in Item doctype (backport #53530) (#53532)
Co-authored-by: Abdus Samad <120767334+Samad-11@users.noreply.github.com>
fix: add item_name to quick entry fields in Item doctype (#53530)
2026-03-17 05:42:21 +00:00
mergify[bot]
d262a65b00 fix: correct overlap detection in JobCard.has_overlap (backport #53473) (#53522)
Co-authored-by: Sanjesh-Raju <sanjesh@tridotstech.com>
Co-authored-by: Sanjesh <rsanjesh64@gmail.com>
Co-authored-by: Tridots Tech <info@tridotstech.com>
fix: correct overlap detection in JobCard.has_overlap (#53473)
2026-03-17 10:42:51 +05:30
Nikhil Kothari
ab9d960aa8 fix(banking): include paid purchase invoices in reports and bank clearance (#52675)
* fix(banking): include paid purchase invoices in reports and bank clearance

* fix: condition for amounts not reflected in system

* fix: set Sales Invoice to be the payment document in bank rec

* fix: add additional filter for `is_paid`

* fix: added is_paid

* fix: added invoice number in bank clearance tool

* chore: make requested changes

* fix: exclude opening JEs

* fix: bring back banking icon in desktop

(cherry picked from commit ef32622166)

# Conflicts:
#	erpnext/accounts/doctype/bank_clearance/bank_clearance.py
#	erpnext/desktop_icon/banking.json
2026-03-17 04:48:25 +00:00
mergify[bot]
eec8cf8a71 fix: change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (backport #53503) (#53516)
* fix: change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (#53503)

(cherry picked from commit 4cd150ba7a)

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

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Abdus Samad <120767334+Samad-11@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-16 18:33:11 +00:00
Rohit Waghchaure
284ccd1def fix: do not set valuation rate for invoice without update stock
(cherry picked from commit bec9e48435)
2026-03-16 18:03:28 +00:00
rohitwaghchaure
3aafed0659 Merge pull request #53497 from frappe/mergify/bp/version-15-hotfix/pr-53495
fix: stock adjustment entry (backport #53495)
2026-03-16 18:07:13 +05:30
mergify[bot]
30fe711c44 fix(support-settings): disable the auto-close tickets feature if close_issue_after_days is set to 0 (backport #53499) (#53504)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(support-settings): disable the auto-close tickets feature if `close_issue_after_days` is set to 0 (#53499)
2026-03-16 12:12:04 +00:00
Rohit Waghchaure
ac6c06daf9 fix: stock adjustment entry
(cherry picked from commit af3067ee23)
2026-03-16 09:47:52 +00:00
mergify[bot]
b63b5320f2 fix(p&l_statement): disable accumulated value filter by default (backport #53488) (#53489)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(p&l_statement): disable accumulated value filter by default (#53488)
2026-03-16 12:53:12 +05:30
ruthra kumar
433dec8a6c Merge pull request #53418 from frappe/mergify/bp/version-15-hotfix/pr-53415
fix: broke cost center filter in get outstanding reference docs (backport #53415)
2026-03-16 10:09:03 +05:30
mergify[bot]
81244a84e7 chore: add docs to project URLs (backport #53467) (#53468)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-03-15 14:53:39 +01:00
mergify[bot]
7a7c4a03f0 fix(serial_and_batch_bundle_selector): handle CSV attachment properly (backport #53460) (#53461)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(serial_and_batch_bundle_selector): handle CSV attachment properly (#53460)
2026-03-15 07:45:00 +00:00
mergify[bot]
a6cf31edad fix: sales order indicator should be based on available qty rather th… (backport #53456) (#53457) 2026-03-15 09:46:07 +05:30
ruthra kumar
d6693c9b79 Merge pull request #53425 from frappe/mergify/bp/version-15-hotfix/pr-53423
refactor: disable total row in trends report (backport #53423)
2026-03-13 18:18:26 +05:30
ruthra kumar
56ffd52335 refactor: disable total row in trends report
(cherry picked from commit 4dbc72b301)
2026-03-13 12:32:55 +00:00
ruthra kumar
53e3bfbf22 fix: broke cost center filter in get outstanding reference docs
(cherry picked from commit 7dfe36fdce)
2026-03-13 09:55:16 +00:00
mergify[bot]
db9dc86694 Revert "fix(regional): rename duplicate Customer fields in Italy setup" (backport #53409) (#53410)
* Revert "fix(regional): rename duplicate Customer fields in Italy setup" (#53409)

(cherry picked from commit bd87a7e612)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-13 07:47:07 +00:00
Khushi Rawat
1c444ef822 Merge pull request #53407 from khushi8112/show-asset-purchase-amount-currency
fix: update label on company change
2026-03-13 12:42:02 +05:30
khushi8112
f702a71126 chore: linters check 2026-03-13 12:22:45 +05:30
khushi8112
908e185cfe fix: update label on company change 2026-03-13 11:59:42 +05:30
mergify[bot]
2a70203cab fix(regional): rename duplicate Customer fields in Italy setup (backport #50921) (#53397)
* fix(regional): rename duplicate Customer fields in Italy setup (#50921)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit c6efc403cd)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

* chore: resolve conflicts

Removed obsolete patches for older versions.

---------

Co-authored-by: Solede <lorenzo.caldara@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-12 15:20:22 +00:00
Mihir Kandoi
85c4cc3e1b fix: update delivery date in line items (#53331) 2026-03-12 20:37:52 +05:30
Khushi Rawat
62280c285f Merge pull request #53377 from frappe/asset-repair-show-general-ledger
fix: use completion_date not posting date
2026-03-12 15:10:04 +05:30
mergify[bot]
5737d2afa3 fix: precision issue in production plan (backport #53370) (#53373)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: precision issue in production plan (#53370)
2026-03-12 09:32:09 +00:00
mergify[bot]
0e00ab8865 fix: do not modify rate in the child item merely for comparison (backport #53301) (#53375)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: do not modify rate in the child item merely for comparison (#53301)
2026-03-12 09:20:39 +00:00
Khushi Rawat
05d614eb04 fix: coderebbit review 2026-03-12 14:41:23 +05:30
khushi8112
6d476604df fix: use completion_date not posting date 2026-03-12 14:24:59 +05:30
Ejaaz Khan
0d527ac8ea Merge pull request #53359 from frappe/mergify/bp/version-15-hotfix/pr-53348
fix: remove redundant pos print format (backport #53348)
2026-03-12 12:53:40 +05:30
mergify[bot]
0612f1c941 fix: NoneType error when template description is to be copied to variant (backport #53358) (#53365)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: NoneType error when template description is to be copied to variant (#53358)
2026-03-12 06:49:53 +00:00
mergify[bot]
dbed426725 refactor: supplier quotation comparision report button should start f… (backport #53361) (#53362)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-03-12 06:19:37 +00:00
Ejaaz Khan
8497d1f8cf fix: remove redundant pos print format (#53348)
(cherry picked from commit e4d79c6246)
2026-03-12 06:02:34 +00:00
mergify[bot]
034d460ae1 fix(delivery note): avoid maintaining si_detail on return delivery note (backport #52456) (#53352)
Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com>
fix(delivery note): avoid maintaining si_detail on return delivery note (#52456)
2026-03-12 05:40:33 +00:00
mergify[bot]
fd94cd0e7c Feat/shipment default contact (backport #53029) (#53354)
Co-authored-by: David <52141166+sdavidbastos@users.noreply.github.com>
2026-03-12 05:40:00 +00:00
mergify[bot]
db251c6e11 fix: re-calculate taxes and totals after resetting bundle item rate (backport #53342) (#53349)
Co-authored-by: V Shankar <shankarv292002@gmail.com>
fix: re-calculate taxes and totals after resetting bundle item rate (#53342)
2026-03-12 05:15:12 +00:00
Nihantra C. Patel
b037dae529 Merge pull request #53335 from frappe/mergify/bp/version-15-hotfix/pr-53327
fix: Append existing ignored doctypes in Journal Entry on_cancel instead of overwriting (backport #53327)
2026-03-11 16:29:11 +05:30
Nihantra Patel
b73d9700d0 fix: Append existing ignored doctypes in Journal Entry on_cancel instead of overwriting
(cherry picked from commit 39e10c4ab0)
2026-03-11 10:41:30 +00:00
ruthra kumar
73d347f456 Merge pull request #53328 from frappe/mergify/bp/version-15-hotfix/pr-53326
refactor: make cost center editable in payment entry deduction (backport #53326)
2026-03-11 15:08:34 +05:30
ruthra kumar
3e7d2c6f11 refactor: make cost center editable in payment entry deduction
(cherry picked from commit 078b22d985)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
2026-03-11 14:53:09 +05:30
Nishka Gosalia
4ebc23752e Merge pull request #53324 from frappe/mergify/bp/version-15-hotfix/pr-53312
fix: update item description in Production Plan Assembly Items table (backport #53312)
2026-03-11 14:35:10 +05:30
Parameshwari Palanisamy
ef6fd7dcb5 Update production_plan.py
(cherry picked from commit 39e68a9ce7)
2026-03-11 08:49:06 +00:00
creative-paramu
e3e9d7b19e fix: update item description in Production Plan Assembly Items table
(cherry picked from commit 19533551f4)
2026-03-11 08:49:06 +00:00
NaviN
a4aaf67b2b Merge pull request #52964 from ljain112/fix-tds-party
fix(tds-report): correct party type filtering and refactor
2026-03-11 11:41:44 +05:30
Jatin3128
d425e90ef7 Merge pull request #53213 from Jatin3128/fix-53173
fix: correct payment terms fetching and recalculation logic
2026-03-11 02:56:03 +05:30
Jatin3128
79b04826d9 fix: correct payment terms fetching and recalculation logic 2026-03-11 02:55:40 +05:30
NaviN
8c7100df04 Merge pull request #53279 from frappe/mergify/bp/version-15-hotfix/pr-53203
fix: update user status depends on employee status (backport #53203)
2026-03-10 22:41:20 +05:30
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
Navin-S-R
55a0603356 chore: resolve conflict 2026-03-10 17:58:57 +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
Poovitha Palanivelu
c5796fed4a fix: update user status depends on employee status
(cherry picked from commit 194d060f13)

# Conflicts:
#	erpnext/setup/doctype/employee/employee.py
2026-03-10 07:39:56 +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
94972da845 fix: correct function syntax in TDS Computation Report 2026-03-09 19:25:38 +05:30
ljain112
c2e67599f5 fix: refactor GL entry mapping to include voucher type 2026-03-09 19:14:21 +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
93ebec90ef fix: enhance sorting and optimize GL entry retrieval 2026-03-09 18:50:42 +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
Jatin3128
3148816451 fix: correct payment terms fetching and recalculation logic 2026-03-06 15:08:35 +05:30
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
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
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
ljain112
e5eb5406da fix(tds-report): correct party type filtering and refactor 2026-02-25 16:30:30 +05:30
168 changed files with 4645 additions and 1457 deletions

View File

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

View File

@@ -216,7 +216,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order"
"label": "Automatically Fetch Payment Terms from Order/Quotation"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -307,7 +307,7 @@
},
{
"default": "0",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
@@ -671,7 +671,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-12-26 19:46:55.093717",
"modified": "2026-03-06 14:49:11.467716",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -701,4 +701,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

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

View File

@@ -5,7 +5,9 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from pypika import Order
@@ -136,65 +138,162 @@ def get_payment_entries_for_bank_clearance(
):
entries = []
condition = ""
pe_condition = ""
journal_entry = frappe.qb.DocType("Journal Entry")
journal_entry_account = frappe.qb.DocType("Journal Entry Account")
journal_entry_query = (
frappe.qb.from_(journal_entry_account)
.inner_join(journal_entry)
.on(journal_entry_account.parent == journal_entry.name)
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
journal_entry.name.as_("payment_entry"),
journal_entry.cheque_no.as_("cheque_number"),
journal_entry.cheque_date,
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
journal_entry.posting_date,
journal_entry_account.against_account,
journal_entry.clearance_date,
journal_entry_account.account_currency,
)
.where(
(journal_entry_account.account == account)
& (journal_entry.docstatus == 1)
& (journal_entry.posting_date >= from_date)
& (journal_entry.posting_date <= to_date)
& (journal_entry.is_opening == "No")
)
)
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
journal_entry_query = journal_entry_query.where(
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
)
journal_entries = frappe.db.sql(
f"""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
journal_entries = (
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
.orderby(journal_entry.posting_date)
.orderby(journal_entry.name, order=Order.desc)
).run(as_dict=True)
pe = frappe.qb.DocType("Payment Entry")
company = frappe.qb.DocType("Company")
payment_entry_query = (
frappe.qb.from_(pe)
.join(company)
.on(pe.company == company.name)
.select(
ConstantColumn("Payment Entry").as_("payment_document"),
pe.name.as_("payment_entry"),
pe.reference_no.as_("cheque_number"),
pe.reference_date.as_("cheque_date"),
(
Case()
.when(
pe.paid_from == account,
(
pe.paid_amount
+ (
Case()
.when(
(pe.payment_type == "Pay")
& (company.default_currency == pe.paid_from_account_currency),
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
),
)
.else_(0)
).as_("credit"),
(
Case()
.when(pe.paid_from == account, 0)
.else_(
pe.received_amount
+ (
Case()
.when(
company.default_currency == pe.paid_to_account_currency,
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
)
).as_("debit"),
pe.posting_date,
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
"against_account"
),
pe.clearance_date,
(
Case()
.when(pe.paid_to == account, pe.paid_to_account_currency)
.else_(pe.paid_from_account_currency)
).as_("account_currency"),
)
.where(
((pe.paid_from == account) | (pe.paid_to == account))
& (pe.docstatus == 1)
& (pe.posting_date >= from_date)
& (pe.posting_date <= to_date)
)
)
payment_entries = frappe.db.sql(
f"""
select
"Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
from `tabPayment Entry` as pe
join `tabCompany` c on c.name = pe.company
where
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
{pe_condition}
order by
pe.posting_date ASC, pe.name DESC
""",
{
"account": account,
"from": from_date,
"to": to_date,
},
as_dict=1,
if not include_reconciled_entries:
payment_entry_query = payment_entry_query.where(
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
)
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
as_dict=True
)
pos_sales_invoices, pos_purchase_invoices = [], []
acc = frappe.qb.DocType("Account")
pi = frappe.qb.DocType("Purchase Invoice")
paid_purchase_invoices_query = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.bill_no.as_("cheque_number"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
)
if not include_reconciled_entries:
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
)
paid_purchase_invoices = (
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
).run(as_dict=True)
pos_sales_invoices = []
if include_pos_transactions:
si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice")
acc = frappe.qb.DocType("Account")
pos_sales_invoices = (
pos_sales_invoices_query = (
frappe.qb.from_(si_payment)
.inner_join(si)
.on(si_payment.parent == si.name)
@@ -217,38 +316,22 @@ def get_payment_entries_for_bank_clearance(
& (si.posting_date >= from_date)
& (si.posting_date <= to_date)
)
.orderby(si.posting_date)
.orderby(si.name, order=Order.desc)
).run(as_dict=True)
)
pi = frappe.qb.DocType("Purchase Invoice")
if not include_reconciled_entries:
pos_sales_invoices_query = pos_sales_invoices_query.where(
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
)
pos_purchase_invoices = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
pos_sales_invoices = (
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
).run(as_dict=True)
entries = (
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(paid_purchase_invoices)
)
return entries

View File

@@ -2,6 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on("Bank Statement Import", {
onload(frm) {
frm.set_query("bank_account", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
},
setup(frm) {
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
frm.import_in_progress = false;

View File

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

View File

@@ -214,6 +214,8 @@ class JournalEntry(AccountsController):
def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super().on_cancel()
from_doc_events = getattr(self, "ignore_linked_doctypes", ())
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
@@ -226,6 +228,10 @@ class JournalEntry(AccountsController):
"Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
)
if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
self.make_gl_entries(1)
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
@@ -263,6 +269,9 @@ class JournalEntry(AccountsController):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
if not erpnext.is_perpetual_inventory_enabled(self.company):
return
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(

View File

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

View File

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

View File

@@ -839,7 +839,7 @@ frappe.ui.form.on("Payment Entry", {
paid_amount: function (frm) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (!frm.doc.received_amount) {
if (frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
@@ -860,7 +860,7 @@ frappe.ui.form.on("Payment Entry", {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
if (!frm.doc.paid_amount) {
if (frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount);
if (frm.doc.target_exchange_rate) {

View File

@@ -2556,14 +2556,9 @@ def get_orders_to_be_billed(
if not voucher_type:
return []
# Add cost center condition
doc = frappe.get_doc({"doctype": voucher_type})
condition = ""
if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
condition = ""
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"

View File

@@ -2043,6 +2043,7 @@ def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
customer = customer.name
return customer

View File

@@ -22,6 +22,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
@@ -59,7 +60,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-05 16:07:47.307971",
"modified": "2026-03-11 14:26:11.312950",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -152,7 +152,9 @@ class ShippingRule(Document):
frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges"
shipping_charge["category"] = "Valuation and Total"
shipping_charge["category"] = (
"Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total"
)
shipping_charge["add_deduct_tax"] = "Add"
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)

View File

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

View File

@@ -1,23 +0,0 @@
{
"align_labels_right": 0,
"creation": "2016-05-05 17:16:18.564460",
"custom_format": 1,
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ company }}<br>\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}<br>\n</p>\n<p>\n\t<b>{{ __(\"Customer\") }}:</b> {{ customer }}<br>\n</p>\n\n<p>\n\t<b>{{ __(\"Date\") }}:</b> {{ dateutil.global_date_format(posting_date) }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ __(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{% for item in items %}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_name }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ format_number(item.qty, null,precision(\"difference\")) }}<br>@ {{ format_currency(item.rate, currency) }}</td>\n\t\t\t<td class=\"text-right\">{{ format_currency(item.amount, currency) }}</td>\n\t\t</tr>\n\t\t{% endfor %}\n\t</tbody>\n</table>\n\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ row.description }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Qty Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ qty_total }}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n\n<hr>\n<p>{{ terms }}</p>\n<p class=\"text-center\">{{ __(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"modified": "2019-09-05 17:20:30.726659",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Point of Sale",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "JS",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -779,6 +779,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
"customer_group": "Individual",
}
)
.insert()
@@ -1002,6 +1003,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
"customer_group": "Individual",
}
)
.insert()

View File

@@ -4,7 +4,10 @@
import frappe
from frappe import _
from frappe.utils import getdate, nowdate
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import getdate
from pypika import Order
def execute(filters=None):
@@ -48,17 +51,6 @@ def get_columns():
return columns
def get_conditions(filters):
conditions = ""
if filters.get("from_date"):
conditions += " and posting_date>=%(from_date)s"
if filters.get("to_date"):
conditions += " and posting_date<=%(to_date)s"
return conditions
def get_entries(filters):
entries = []
@@ -73,41 +65,90 @@ def get_entries(filters):
return sorted(
entries,
key=lambda k: k[2].strftime("%H%M%S") or getdate(nowdate()),
key=lambda k: getdate(k[2]),
)
def get_entries_for_bank_clearance_summary(filters):
entries = []
conditions = get_conditions(filters)
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
journal_entries = frappe.db.sql(
f"""SELECT
"Journal Entry", jv.name, jv.posting_date, jv.cheque_no,
jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit
FROM
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
WHERE
jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {conditions}
order by posting_date DESC, jv.name DESC""",
filters,
as_list=1,
)
journal_entries = (
frappe.qb.from_(jea)
.inner_join(je)
.on(jea.parent == je.name)
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
je.name.as_("payment_entry"),
je.posting_date,
je.cheque_no,
je.clearance_date,
jea.against_account,
jea.debit_in_account_currency - jea.credit_in_account_currency,
)
.where(
(jea.account == filters.account)
& (je.docstatus == 1)
& (je.posting_date >= filters.from_date)
& (je.posting_date <= filters.to_date)
& ((je.is_opening == "No") | (je.is_opening.isnull()))
)
.orderby(je.posting_date, order=Order.desc)
.orderby(je.name, order=Order.desc)
).run(as_list=True)
payment_entries = frappe.db.sql(
f"""SELECT
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
FROM
`tabPayment Entry`
WHERE
docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {conditions}
order by posting_date DESC, name DESC""",
filters,
as_list=1,
)
pe = frappe.qb.DocType("Payment Entry")
payment_entries = (
frappe.qb.from_(pe)
.select(
ConstantColumn("Payment Entry").as_("payment_document"),
pe.name.as_("payment_entry"),
pe.posting_date,
pe.reference_no.as_("cheque_no"),
pe.clearance_date,
pe.party.as_("against_account"),
Case()
.when(
(pe.paid_from == filters.account),
((pe.paid_amount * -1) - pe.total_taxes_and_charges),
)
.else_(pe.received_amount),
)
.where((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
.where(
(pe.docstatus == 1)
& (pe.posting_date >= filters.from_date)
& (pe.posting_date <= filters.to_date)
)
.orderby(pe.posting_date, order=Order.desc)
.orderby(pe.name, order=Order.desc)
).run(as_list=True)
entries = journal_entries + payment_entries
pi = frappe.qb.DocType("Purchase Invoice")
purchase_invoices = (
frappe.qb.from_(pi)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.posting_date,
pi.bill_no.as_("cheque_no"),
pi.clearance_date,
pi.supplier.as_("against_account"),
(pi.paid_amount * -1).as_("amount"),
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == filters.account)
& (pi.posting_date >= filters.from_date)
& (pi.posting_date <= filters.to_date)
)
.orderby(pi.posting_date, order=Order.desc)
.orderby(pi.name, order=Order.desc)
).run(as_list=True)
entries = journal_entries + payment_entries + purchase_invoices
return entries

View File

@@ -4,7 +4,11 @@
import frappe
from frappe import _
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, getdate
from pypika import Order
from erpnext.accounts.utils import get_balance_on
@@ -123,73 +127,143 @@ def get_entries_for_bank_reconciliation_statement(filters):
payment_entries = get_payment_entries(filters)
purchase_invoices = get_purchase_invoices(filters)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
return list(journal_entries) + list(payment_entries) + list(pos_entries)
return list(journal_entries) + list(payment_entries) + list(pos_entries) + list(purchase_invoices)
def get_journal_entries(filters):
return frappe.db.sql(
"""
select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account,
jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency
from
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
where jvd.parent = jv.name and jv.docstatus=1
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'
and jv.company = %(company)s """,
filters,
as_dict=1,
)
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
return (
frappe.qb.from_(jea)
.join(je)
.on(jea.parent == je.name)
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
je.name.as_("payment_entry"),
je.posting_date,
jea.debit_in_account_currency.as_("debit"),
jea.credit_in_account_currency.as_("credit"),
jea.against_account,
je.cheque_no.as_("reference_no"),
je.cheque_date.as_("ref_date"),
je.clearance_date,
jea.account_currency,
)
.where(
(je.docstatus == 1)
& (jea.account == filters.account)
& (je.posting_date <= filters.report_date)
& (je.clearance_date.isnull() | (je.clearance_date > filters.report_date))
& (je.company == filters.company)
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
)
.orderby(je.posting_date)
.orderby(je.name, order=Order.desc)
).run(as_dict=True)
def get_payment_entries(filters):
return frappe.db.sql(
"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date,
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date <= %(report_date)s
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
and company = %(company)s
""",
filters,
as_dict=1,
)
pe = frappe.qb.DocType("Payment Entry")
return (
frappe.qb.from_(pe)
.select(
ConstantColumn("Payment Entry").as_("payment_document"),
pe.name.as_("payment_entry"),
pe.reference_no.as_("reference_no"),
pe.reference_date.as_("ref_date"),
Case().when(pe.paid_to == filters.account, pe.received_amount_after_tax).else_(0).as_("debit"),
Case().when(pe.paid_from == filters.account, pe.paid_amount_after_tax).else_(0).as_("credit"),
pe.posting_date,
Coalesce(
pe.party, Case().when(pe.paid_from == filters.account, pe.paid_to).else_(pe.paid_from)
).as_("against_account"),
pe.clearance_date,
(
Case()
.when(pe.paid_to == filters.account, pe.paid_to_account_currency)
.else_(pe.paid_from_account_currency)
).as_("account_currency"),
)
.where(
(pe.docstatus == 1)
& ((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
& (pe.posting_date <= filters.report_date)
& (pe.clearance_date.isnull() | (pe.clearance_date > filters.report_date))
& (pe.company == filters.company)
)
.orderby(pe.posting_date)
.orderby(pe.name, order=Order.desc)
).run(as_dict=True)
def get_purchase_invoices(filters):
pi = frappe.qb.DocType("Purchase Invoice")
acc = frappe.qb.DocType("Account")
return (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.bill_no.as_("reference_no"),
pi.posting_date.as_("ref_date"),
Case().when(pi.paid_amount < 0, pi.paid_amount * -1).else_(0).as_("debit"),
Case().when(pi.paid_amount > 0, pi.paid_amount).else_(0).as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == filters.account)
& (pi.posting_date <= filters.report_date)
& (pi.clearance_date.isnull() | (pi.clearance_date > filters.report_date))
& (pi.company == filters.company)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
).run(as_dict=True)
def get_pos_entries(filters):
return frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date <= %(report_date)s and
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
and si.company = %(company)s
order by
si.posting_date ASC, si.name DESC
""",
filters,
as_dict=1,
)
si = frappe.qb.DocType("Sales Invoice")
si_payment = frappe.qb.DocType("Sales Invoice Payment")
acc = frappe.qb.DocType("Account")
return (
frappe.qb.from_(si_payment)
.join(si)
.on(si_payment.parent == si.name)
.join(acc)
.on(si_payment.account == acc.name)
.select(
ConstantColumn("Sales Invoice").as_("payment_document"),
si.name.as_("payment_entry"),
si_payment.amount.as_("debit"),
si.posting_date,
si.debit_to.as_("against_account"),
si_payment.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("credit"),
)
.where(
(si_payment.account == filters.account)
& (si.docstatus == 1)
& (si.posting_date <= filters.report_date)
& (si_payment.clearance_date.isnull() | (si_payment.clearance_date > filters.report_date))
& (si.company == filters.company)
)
.orderby(si.posting_date)
.orderby(si_payment.name, order=Order.desc)
).run(as_dict=True)
def get_amounts_not_reflected_in_system(filters):
@@ -205,30 +279,66 @@ def get_amounts_not_reflected_in_system(filters):
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
je_amount = frappe.db.sql(
"""
select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s
and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No' """,
filters,
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
je_amount = (
frappe.qb.from_(jea)
.inner_join(je)
.on(jea.parent == je.name)
.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("amount"),
)
.where(
(je.docstatus == 1)
& (jea.account == filters.account)
& (je.posting_date > filters.report_date)
& (je.clearance_date <= filters.report_date)
& (je.company == filters.company)
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
)
.run(as_dict=True)
)
je_amount = flt(je_amount[0].amount) if je_amount else 0.0
je_amount = flt(je_amount[0][0]) if je_amount else 0.0
pe_amount = frappe.db.sql(
"""
select sum(if(paid_from=%(account)s, paid_amount, received_amount))
from `tabPayment Entry`
where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date > %(report_date)s and clearance_date <= %(report_date)s""",
filters,
pe = frappe.qb.DocType("Payment Entry")
pe_amount = (
frappe.qb.from_(pe)
.select(
Sum(Case().when(pe.paid_from == filters.account, pe.paid_amount).else_(pe.received_amount)).as_(
"amount"
),
)
.where(
((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
& (pe.docstatus == 1)
& (pe.posting_date > filters.report_date)
& (pe.clearance_date <= filters.report_date)
& (pe.company == filters.company)
)
.run(as_dict=True)
)
pe_amount = flt(pe_amount[0].amount) if pe_amount else 0.0
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
pi = frappe.qb.DocType("Purchase Invoice")
pi_amount = (
frappe.qb.from_(pi)
.select(
Sum(pi.paid_amount).as_("amount"),
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == filters.account)
& (pi.posting_date > filters.report_date)
& (pi.clearance_date <= filters.report_date)
& (pi.company == filters.company)
)
).run(as_dict=True)
return je_amount + pe_amount
pi_amount = flt(pi_amount[0].amount) if pi_amount else 0.0
return je_amount + pe_amount + pi_amount
def get_balance_row(label, amount, account_currency):

View File

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

View File

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

View File

@@ -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

@@ -649,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)
@@ -659,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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ frappe.query_reports["Profit and Loss Statement"]["filters"].push(
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
default: 0,
},
{
fieldname: "include_default_book_entries",

View File

@@ -7,10 +7,10 @@
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"idx": 4,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:49.950442",
"letter_head": null,
"modified": "2026-03-13 17:35:39.703838",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Trends",

View File

@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:50.070651",
"letter_head": null,
"modified": "2026-03-13 17:36:13.725601",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Trends",

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt, getdate
from pypika import Tuple
from erpnext.accounts.utils import get_currency_precision
@@ -19,18 +20,14 @@ def execute(filters=None):
validate_filters(filters)
(
tds_docs,
tds_accounts,
tax_category_map,
journal_entry_party_map,
net_total_map,
) = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
return columns, res
@@ -41,27 +38,23 @@ def validate_filters(filters):
frappe.throw(_("From Date must be before To Date"))
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map):
party_map = get_party_pan_map(filters.get("party_type"))
def get_result(filters, tds_accounts, tax_category_map, net_total_map):
party_names = {v.party for v in net_total_map.values() if v.party}
party_map = get_party_pan_map(filters.get("party_type"), party_names)
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
gle_map = get_gle_map(net_total_map)
precision = get_currency_precision()
out = []
entries = {}
for name, details in gle_map.items():
for (voucher_type, name), details in gle_map.items():
for entry in details:
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
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
party_list = journal_entry_party_map.get(name)
if party_list:
party = party_list[0]
values = net_total_map.get((voucher_type, name))
party = values.party if values else (entry.party or entry.against)
if entry.account in tds_accounts.keys():
tax_amount += entry.credit - entry.debit
@@ -76,12 +69,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
values = net_total_map.get((voucher_type, name))
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
base_total = min(
flt(tax_amount / (rate / 100), precision=precision),
values.base_tax_withholding_net_total,
)
total_amount = grand_total = base_total
base_tax_withholding_net_total = total_amount
@@ -90,16 +84,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
total_amount = values.base_tax_withholding_net_total
grand_total = values[1]
base_total = values[2]
grand_total = values.grand_total
base_total = values.base_total
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]
base_tax_withholding_net_total = values.base_tax_withholding_net_total
bill_no = values.bill_no
bill_date = values.bill_date
else:
total_amount += entry.credit
@@ -147,14 +141,17 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
else:
entries[key] = row
out = list(entries.values())
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
return out
def get_party_pan_map(party_type):
def get_party_pan_map(party_type, party_names):
party_map = frappe._dict()
if not party_names:
return party_map
fields = ["name", "tax_withholding_category"]
if party_type == "Supplier":
fields += ["supplier_type", "supplier_name"]
@@ -164,7 +161,7 @@ def get_party_pan_map(party_type):
if frappe.db.has_column(party_type, "pan"):
fields.append("pan")
party_details = frappe.db.get_all(party_type, fields=fields)
party_details = frappe.db.get_all(party_type, filters={"name": ("in", list(party_names))}, fields=fields)
for party in party_details:
party.party_type = party_type
@@ -173,22 +170,33 @@ def get_party_pan_map(party_type):
return party_map
def get_gle_map(documents):
# create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice}
def get_gle_map(net_total_map):
if not net_total_map:
return {}
gle = frappe.qb.DocType("GL Entry")
voucher_pairs = list(net_total_map.keys())
rows = (
frappe.qb.from_(gle)
.select(
gle.credit,
gle.debit,
gle.account,
gle.voucher_no,
gle.posting_date,
gle.voucher_type,
gle.against,
gle.party,
gle.party_type,
)
.where(gle.is_cancelled == 0)
.where(Tuple(gle.voucher_type, gle.voucher_no).isin(voucher_pairs))
).run(as_dict=True)
gle_map = {}
gle = frappe.db.get_all(
"GL Entry",
{"voucher_no": ["in", documents], "is_cancelled": 0},
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
)
for d in gle:
if d.voucher_no not in gle_map:
gle_map[d.voucher_no] = [d]
else:
gle_map[d.voucher_no].append(d)
for d in rows:
gle_map.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gle_map
@@ -308,14 +316,9 @@ def get_columns(filters):
def get_tds_docs(filters):
tds_documents = []
purchase_invoices = []
sales_invoices = []
payment_entries = []
journal_entries = []
vouchers = frappe._dict()
tax_category_map = frappe._dict()
net_total_map = frappe._dict()
journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
_tds_accounts = frappe.get_all(
@@ -334,35 +337,14 @@ def get_tds_docs(filters):
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
purchase_invoices.append(d.voucher_no)
if d.voucher_type == "Sales Invoice":
sales_invoices.append(d.voucher_no)
elif d.voucher_type == "Payment Entry":
payment_entries.append(d.voucher_no)
elif d.voucher_type == "Journal Entry":
journal_entries.append(d.voucher_no)
vouchers.setdefault(d.voucher_type, set()).add(d.voucher_no)
tds_documents.append(d.voucher_no)
if purchase_invoices:
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
if sales_invoices:
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
if payment_entries:
get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
for voucher_type, docs in vouchers.items():
get_doc_info(docs, voucher_type, tax_category_map, net_total_map, filters)
return (
tds_documents,
tds_accounts,
tax_category_map,
journal_entry_party_map,
net_total_map,
)
@@ -373,11 +355,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
title=_("Accounts Missing Error"),
)
invoice_voucher = "Purchase Invoice" if filters.get("party_type") == "Supplier" else "Sales Invoice"
voucher_types = {"Payment Entry", "Journal Entry", invoice_voucher}
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select("voucher_no", "voucher_type", "against", "party")
.where(gle.is_cancelled == 0)
.where(gle.voucher_type.isin(voucher_types))
)
if filters.get("from_date"):
@@ -403,25 +390,27 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
return query
def get_journal_entry_party_map(journal_entries):
def get_journal_entry_party_map(journal_entries, party_type):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{
"parent": ("in", journal_entries),
"party_type": ("in", ("Supplier", "Customer")),
"party_type": party_type,
"party": ("is", "set"),
},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
journal_entry_party_map[d.parent] = []
journal_entry_party_map[d.parent].append(d.party)
journal_entry_party_map.setdefault(d.parent, []).append(d.party)
return journal_entry_party_map
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None, filters=None):
journal_entry_party_map = {}
party_type = filters.get("party_type") if filters else None
party = filters.get("party") if filters else None
common_fields = ["name"]
fields_dict = {
"Purchase Invoice": [
@@ -431,37 +420,81 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"base_total",
"bill_no",
"bill_date",
"supplier",
],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Sales Invoice": ["base_net_total", "grand_total", "base_total", "customer"],
"Payment Entry": [
"tax_withholding_category",
"paid_amount",
"paid_amount_after_tax",
"base_paid_amount",
"party",
"party_type",
],
"Journal Entry": ["tax_withholding_category", "total_debit"],
}
party_field = {
"Purchase Invoice": "supplier",
"Sales Invoice": "customer",
"Payment Entry": "party",
}
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
doc_filters = {"name": ("in", vouchers)}
if party and party_field.get(doctype):
doc_filters[party_field[doctype]] = party
if doctype == "Payment Entry":
doc_filters["party_type"] = party_type
entries = frappe.get_all(doctype, filters=doc_filters, fields=common_fields + fields_dict[doctype])
if doctype == "Journal Entry":
journal_entry_party_map = get_journal_entry_party_map(vouchers, party_type=party_type)
for entry in entries:
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
value = frappe._dict(
party=None,
party_type=party_type,
base_tax_withholding_net_total=0,
grand_total=0,
base_total=0,
bill_no="",
bill_date="",
)
if doctype == "Purchase Invoice":
value = [
entry.base_tax_withholding_net_total,
entry.grand_total,
entry.base_total,
entry.bill_no,
entry.bill_date,
]
value.party = entry.supplier
value.party_type = "Supplier"
value.base_tax_withholding_net_total = entry.base_tax_withholding_net_total
value.grand_total = entry.grand_total
value.base_total = entry.base_total
value.bill_no = entry.bill_no
value.bill_date = entry.bill_date
elif doctype == "Sales Invoice":
value = [entry.base_net_total, entry.grand_total, entry.base_total]
value.party = entry.customer
value.party_type = "Customer"
value.base_tax_withholding_net_total = entry.base_net_total
value.grand_total = entry.grand_total
value.base_total = entry.base_total
elif doctype == "Payment Entry":
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
value.party = entry.party
value.party_type = entry.party_type
value.base_tax_withholding_net_total = entry.paid_amount
value.grand_total = entry.paid_amount_after_tax
value.base_total = entry.base_paid_amount
else:
value = [entry.total_debit] * 3
party_list = journal_entry_party_map.get(entry.name, [])
if party and party in party_list:
value.party = party
elif party_list:
value.party = sorted(party_list)[0]
value.party_type = party_type
value.base_tax_withholding_net_total = entry.total_debit
value.grand_total = entry.total_debit
value.base_total = entry.total_debit
net_total_map[(doctype, entry.name)] = value

View File

@@ -20,16 +20,12 @@ def execute(filters=None):
columns = get_columns(filters)
(
tds_docs,
tds_accounts,
tax_category_map,
journal_entry_party_map,
invoice_total_map,
net_total_map,
) = get_tds_docs(filters)
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
)
res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
final_result = group_by_party_and_category(res, filters)
return columns, final_result

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ frappe.ui.form.on("Asset", {
},
company: function (frm) {
frm.trigger("set_dynamic_labels");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
@@ -87,6 +88,7 @@ frappe.ui.form.on("Asset", {
},
refresh: function (frm) {
frm.trigger("set_dynamic_labels");
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -221,6 +223,10 @@ frappe.ui.form.on("Asset", {
}
},
set_dynamic_labels: function (frm) {
frm.set_currency_labels(["gross_purchase_amount"], erpnext.get_currency(frm.doc.company));
},
set_depr_posting_failure_alert: function (frm) {
const alert = `
<div class="row">

View File

@@ -229,7 +229,7 @@
"fieldtype": "Currency",
"label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency",
"options": "currency",
"read_only_depends_on": "eval: doc.is_composite_asset"
},
{
@@ -597,7 +597,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-12-23 16:01:10.195932",
"modified": "2026-03-13 12:15:25.734623",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

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 {
@@ -135,7 +131,7 @@ frappe.ui.form.on("Asset Repair", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
from_date: moment(frm.doc.completion_date).format("YYYY-MM-DD"),
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",

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

@@ -769,10 +769,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
items_on_form_rendered() {
set_schedule_date(this.frm);
}
schedule_date() {
set_schedule_date(this.frm);
}
};
// for backward compatibility: combine new and previous states

View File

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

View File

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

View File

@@ -165,14 +165,10 @@ frappe.ui.form.on("Request for Quotation", {
},
show_supplier_quotation_comparison(frm) {
const today = new Date();
const oneMonthAgo = new Date(today);
oneMonthAgo.setMonth(today.getMonth() - 1);
frappe.route_options = {
company: frm.doc.company,
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
to_date: moment(today).format("YYYY-MM-DD"),
from_date: moment(frm.doc.transaction_date).format("YYYY-MM-DD"),
to_date: moment(new Date()).format("YYYY-MM-DD"),
request_for_quotation: frm.doc.name,
};
frappe.set_route("query-report", "Supplier Quotation Comparison");

View File

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

View File

@@ -37,7 +37,7 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(sq.get("items")[0].qty, 5)
self.assertEqual(sq.get("items")[1].rate, 300)
def test_update_supplier_quotation_child_rate_disallow(self):
def test_update_supplier_quotation_child_rate(self):
sq = frappe.copy_doc(test_records[0])
sq.submit()
trans_item = json.dumps(
@@ -50,6 +50,22 @@ class TestPurchaseOrder(FrappeTestCase):
},
]
)
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
)

View File

@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:50.058154",
"letter_head": null,
"modified": "2026-03-13 17:36:05.561765",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Trends",

View File

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

View File

@@ -2297,6 +2297,16 @@ class AccountsController(TransactionBase):
return stock_items
def get_asset_items(self):
asset_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
asset_items = frappe.db.get_values(
"Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True
)
return asset_items
def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
return (
@@ -2502,13 +2512,14 @@ class AccountsController(TransactionBase):
grand_total = self.get("rounded_total") or self.grand_total
automatically_fetch_payment_terms = 0
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
po_or_so, doctype, fieldname = self.get_order_details()
automatically_fetch_payment_terms = cint(
frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
if self.doctype != "Sales Order":
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"):
if party_account_currency == self.company_currency:
@@ -2524,7 +2535,7 @@ class AccountsController(TransactionBase):
if not self.get("payment_schedule"):
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"]
self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
and automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
@@ -2579,17 +2590,23 @@ class AccountsController(TransactionBase):
self.ignore_default_payment_terms_template = 1
def get_order_details(self):
if not self.get("items"):
return None, None, None
if self.doctype == "Sales Invoice":
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"
prev_doc = self.get("items")[0].get("sales_order")
prev_doctype = "Sales Order"
prev_doctype_name = "sales_order"
elif self.doctype == "Purchase Invoice":
prev_doc = self.get("items")[0].get("purchase_order")
prev_doctype = "Purchase Order"
prev_doctype_name = "purchase_order"
elif self.doctype == "Sales Order":
prev_doc = self.get("items")[0].get("prevdoc_docname")
prev_doctype = "Quotation"
prev_doctype_name = "prevdoc_docname"
else:
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"
return po_or_so, po_or_so_doctype, po_or_so_doctype_name
return None, None, None
return prev_doc, prev_doctype, prev_doctype_name
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
@@ -2685,7 +2702,9 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
d.validate_from_to_dates("discount_date", "due_date")
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
self.transaction_date
):
frappe.throw(
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
d.idx
@@ -3838,20 +3857,28 @@ 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 (
@@ -3864,7 +3891,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
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"))
@@ -3980,10 +4015,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
):
continue
validate_quantity(child_item, d)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if not rate_unchanged:
frappe.throw(_("Rates cannot be modified for quoted items"))
validate_quantity_and_rate(child_item, d)
if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True

View File

@@ -327,7 +327,7 @@ class BuyingController(SubcontractingController):
last_item_idx = d.idx
total_valuation_amount = sum(
flt(d.base_tax_amount_after_discount_amount)
flt(d.base_tax_amount_after_discount_amount) * (-1 if d.get("add_deduct_tax") == "Deduct" else 1)
for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]
)
@@ -364,7 +364,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = item.base_net_amount
net_rate = item.qty * item.base_net_rate
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -360,13 +360,13 @@ def copy_attributes_to_variant(item, variant):
else:
if item.variant_based_on == "Item Attribute":
if variant.attributes:
attributes_description = item.description + " "
attributes_description = item.description or ""
for d in variant.attributes:
attributes_description += (
"<div>" + d.attribute + ": " + cstr(d.attribute_value) + "</div>"
)
if attributes_description not in variant.description:
if attributes_description not in (variant.description or ""):
variant.description = attributes_description

View File

@@ -15,6 +15,7 @@ from frappe.utils import cint, nowdate, today, unique
from pypika import Order
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.get_item_details import _get_item_tax_template
@@ -608,34 +609,37 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_income_account(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
# income account can be any Credit account,
# but can also be a Asset account with account_type='Income Account' in special circumstances.
# Hence the first condition is an "OR"
if not filters:
filters = {}
doctype = "Account"
condition = ""
dt = "Account"
acc = qb.DocType(dt)
condition = [
(acc.report_type.eq("Profit and Loss") | acc.account_type.isin(["Income Account", "Temporary"])),
acc.is_group.eq(0),
acc.disabled.eq(0),
]
if txt:
condition.append(acc.name.like(f"%{txt}%"))
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
condition.append(acc.company.eq(filters.get("company")))
condition += " and tabAccount.disabled = %(disabled)s"
user_perms = build_qb_match_conditions(dt)
condition.extend(user_perms)
return frappe.db.sql(
f"""select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss"
or tabAccount.account_type in ("Income Account", "Temporary"))
and tabAccount.is_group=0
and tabAccount.`{searchfield}` LIKE %(txt)s
{condition} {get_match_cond(doctype)}
order by idx desc, name""",
{
"txt": "%" + txt + "%",
"company": filters.get("company", ""),
"disabled": cint(filters.get("disabled", 0)),
},
return (
qb.from_(acc)
.select(acc.name)
.where(Criterion.all(condition))
.orderby(acc.idx, order=Order.desc)
.orderby(acc.name)
.run()
)
@@ -696,26 +700,38 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters,
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
if not filters:
filters = {}
doctype = "Account"
condition = ""
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
dt = "Account"
return frappe.db.sql(
f"""select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss"
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
and tabAccount.is_group=0
and tabAccount.disabled = 0
and tabAccount.{searchfield} LIKE %(txt)s
{condition} {get_match_cond(doctype)}""",
{"company": filters.get("company", ""), "txt": "%" + txt + "%"},
)
acc = qb.DocType(dt)
condition = [
(
acc.report_type.eq("Profit and Loss")
| acc.account_type.isin(
[
"Expense Account",
"Fixed Asset",
"Temporary",
"Asset Received But Not Billed",
"Capital Work in Progress",
]
)
),
acc.is_group.eq(0),
acc.disabled.eq(0),
]
if txt:
condition.append(acc.name.like(f"%{txt}%"))
if filters.get("company"):
condition.append(acc.company.eq(filters.get("company")))
user_perms = build_qb_match_conditions(dt)
condition.extend(user_perms)
return qb.from_(acc).select(acc.name).where(Criterion.all(condition)).run()
@frappe.whitelist()
@@ -976,3 +992,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
limit_page_length=page_len,
as_list=1,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
table = frappe.qb.DocType(doctype)
child_table = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(table)
.inner_join(child_table)
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
.select(table.name)
.where(
(child_table.link_name == filters.get("warehouse"))
& (table.disabled == 0)
& (child_table.link_doctype == "Warehouse")
& (table.name.like(f"%{txt}%"))
)
.offset(start)
.limit(page_len)
)
return query.run(as_list=1)

View File

@@ -531,7 +531,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice
target_doc.so_detail = source_doc.so_detail
target_doc.si_detail = source_doc.si_detail
target_doc.expense_account = source_doc.expense_account
target_doc.dn_detail = source_doc.name
if default_warehouse_for_sales_return:

View File

@@ -511,6 +511,9 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
if self.doctype == "Sales Invoice" and not self.update_stock and not self.is_internal_transfer():
return
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
allow_at_arms_length_price = frappe.get_cached_value(

View File

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

View File

@@ -1135,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)
@@ -1142,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)

View File

@@ -989,6 +989,12 @@ class SubcontractingController(StockController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "BOM"
):
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if not self.__transferred_items or not self.__transferred_items.get(key):
@@ -1297,6 +1303,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,
@@ -1317,53 +1372,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:
@@ -1395,6 +1406,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"
@@ -1405,6 +1418,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,
@@ -1419,27 +1447,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

@@ -183,6 +183,9 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
for item in self.doc.items:
self.doc.round_floats_in(item)
@@ -238,7 +241,13 @@ class calculate_taxes_and_totals:
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
qty = (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice
and self.doc.doctype == "Purchase Receipt"
else item.qty
)
item.amount = flt(item.rate * qty, item.precision("amount"))
item.net_amount = item.amount
@@ -370,9 +379,16 @@ class calculate_taxes_and_totals:
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += item.qty
self.doc.total_qty += (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt"
else item.qty
)
self.doc.base_total += item.base_amount
self.doc.net_total += item.net_amount
self.doc.base_net_total += item.base_net_amount
@@ -724,7 +740,8 @@ class calculate_taxes_and_totals:
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
# only during save (i.e. when `_action` is set)
if self.doc.get("_action") and (
(grand_total >= 0 and discount_amount > grand_total)
or (grand_total < 0 and discount_amount < grand_total) # returns
):

View File

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

View File

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

View File

@@ -55,6 +55,14 @@ def validate_filters(filters):
if filters.get("based_on") == filters.get("group_by"):
frappe.throw(_("'Based On' and 'Group By' can not be same"))
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
frappe.throw(
msg=_("{0} can be either {1} or {2}.").format(
frappe.bold("Period based On"), frappe.bold("Posting Date"), frappe.bold("Billing Date")
),
title=_("Invalid Filter"),
)
def get_data(filters, conditions):
data = []

View File

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

View File

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

View File

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

View File

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

View File

@@ -192,6 +192,7 @@ frappe.ui.form.on("BOM", {
bom_no: frm.doc.name,
item: item,
qty: data.qty || 0.0,
company: frm.doc.company,
project: frm.doc.project,
variant_items: variant_items,
use_multi_level_bom: use_multi_level_bom,
@@ -263,6 +264,7 @@ frappe.ui.form.on("BOM", {
reqd: 1,
default: 1,
onchange: () => {
if (!cur_dialog) return;
const { quantity, items: rm } = frm.doc;
const variant_items_map = rm.reduce((acc, item) => {
acc[item.item_code] = item.qty;

View File

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

View File

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

View File

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

View File

@@ -281,9 +281,8 @@ class JobCard(Document):
# if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
# this will store last to_time of sequential job cards
alloted_capacity = {1: time_logs[0]["to_time"]}
# flag for sequential Job card found
sequential_job_card_found = False
for i in range(1, len(time_logs)):
sequential_job_card_found = False
# scanning for all Existing keys
for key in alloted_capacity.keys():
# if current Job Card from time is greater than last to_time in that key means these job card are sequential
@@ -1076,9 +1075,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

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

View File

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

View File

@@ -1664,8 +1664,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
)
sales_order = data.get("sales_order")
qty_precision = frappe.get_precision("Material Request Plan Item", "quantity")
for item_code, details in item_details.items():
details.qty = flt(details.qty, qty_precision)
so_item_details.setdefault(sales_order, frappe._dict())
if item_code in so_item_details.get(sales_order, {}):
so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
@@ -1784,7 +1786,7 @@ def get_item_data(item_code):
return {
"bom_no": item_details.get("bom_no"),
"stock_uom": item_details.get("stock_uom"),
# "description": item_details.get("description")
"description": item_details.get("description"),
}
@@ -1800,6 +1802,7 @@ def get_sub_assembly_items(
skip_available_sub_assembly_item=False,
):
data = get_bom_children(parent=bom_no)
precision = frappe.get_precision("Production Plan Sub Assembly Item", "qty")
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
@@ -1837,7 +1840,7 @@ def get_sub_assembly_items(
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
"stock_qty": flt(stock_qty, precision),
}
)
)

View File

@@ -4,7 +4,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, nowdate, nowtime, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc
@@ -2395,7 +2395,7 @@ class TestWorkOrder(FrappeTestCase):
stock_entry.submit()
def test_disassembly_order_with_qty_behavior(self):
def test_disassembly_order_with_qty_from_wo_behavior(self):
# Create raw material and FG item
raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name
@@ -2435,27 +2435,9 @@ class TestWorkOrder(FrappeTestCase):
se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
se_for_manufacture.submit()
# Simulate a disassembly stock entry
# Disassembly via WO required_items path (no source_stock_entry)
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
stock_entry.append(
"items",
{
"item_code": fg_item,
"qty": disassemble_qty,
"s_warehouse": wo.fg_warehouse,
},
)
for bom_item in bom.items:
stock_entry.append(
"items",
{
"item_code": bom_item.item_code,
"qty": (bom_item.qty / bom.quantity) * disassemble_qty,
"t_warehouse": wo.source_warehouse,
},
)
wo.reload()
stock_entry.save()
@@ -2470,7 +2452,7 @@ class TestWorkOrder(FrappeTestCase):
f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}",
)
# Assert raw materials
# Assert raw materials - qty scaled from WO required_items
for item in stock_entry.items:
if item.item_code == fg_item:
continue
@@ -2494,10 +2476,35 @@ class TestWorkOrder(FrappeTestCase):
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
)
# Second disassembly: explicitly linked to manufacture SE — verifies SE-linked path
# (first disassembly auto-set source_stock_entry since there's only one manufacture entry)
disassemble_qty_2 = 2
stock_entry_2 = frappe.get_doc(
make_stock_entry(
wo.name, "Disassemble", disassemble_qty_2, source_stock_entry=se_for_manufacture.name
)
)
stock_entry_2.save()
stock_entry_2.submit()
# All rows must trace back to se_for_manufacture
for item in stock_entry_2.items:
self.assertEqual(item.against_stock_entry, se_for_manufacture.name)
self.assertTrue(item.ste_detail)
# RM qty scaled from the manufacture SE rows
rm_row = next((i for i in stock_entry_2.items if i.item_code == raw_item), None)
expected_rm_qty = (bom.items[0].qty / bom.quantity) * disassemble_qty_2
self.assertAlmostEqual(rm_row.qty, expected_rm_qty, places=3)
wo.reload()
self.assertEqual(wo.disassembled_qty, disassemble_qty + disassemble_qty_2)
def test_disassembly_with_multiple_manufacture_entries(self):
"""
Test that disassembly does not create duplicate items when manufacturing
is done in multiple batches (multiple manufacture stock entries).
is done in multiple batches (multiple manufacture stock entries), including
secondary/scrap items.
Scenario:
1. Create Work Order for 10 units
@@ -2506,11 +2513,17 @@ class TestWorkOrder(FrappeTestCase):
4. Create Disassembly for 4 units
5. Verify no duplicate items in the disassembly stock entry
"""
# Create RM and FG item
# Create RM, scrap and FG item
raw_item1 = make_item("Test Raw for Multi Batch Disassembly 1", {"is_stock_item": 1}).name
raw_item2 = make_item("Test Raw for Multi Batch Disassembly 2", {"is_stock_item": 1}).name
scrap_item = make_item("Test Scrap for Multi Batch Disassembly", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Multi Batch Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2)
bom = make_bom(
item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2, do_not_submit=True
)
# add scrap item
bom.append("scrap_items", {"item_code": scrap_item, "stock_qty": 10})
bom.submit()
# Create WO
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
@@ -2585,7 +2598,7 @@ class TestWorkOrder(FrappeTestCase):
f"Found duplicate items in disassembly stock entry: {duplicates}",
)
expected_items = 3 # FG item + 2 raw materials
expected_items = 4 # FG item + 2 raw materials + 1 scrap item
self.assertEqual(
len(stock_entry.items),
expected_items,
@@ -2596,6 +2609,16 @@ class TestWorkOrder(FrappeTestCase):
fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertEqual(fg_item_row.qty, disassemble_qty)
# Scrap item: should be taken from scrap warehouse in disassembly
scrap_row = next((i for i in stock_entry.items if i.item_code == scrap_item), None)
self.assertIsNotNone(scrap_row)
self.assertEqual(scrap_row.is_scrap_item, 1)
self.assertTrue(scrap_row.s_warehouse)
self.assertFalse(scrap_row.t_warehouse)
self.assertEqual(scrap_row.s_warehouse, wo.scrap_warehouse)
# BOM has scrap_qty=10/FG, total produced = 10*10 = 100, disassemble 4/10 → 40
self.assertEqual(scrap_row.qty, 40)
# RM quantities
for bom_item in bom.items:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
@@ -2607,19 +2630,57 @@ class TestWorkOrder(FrappeTestCase):
msg=f"Raw material {bom_item.item_code} qty mismatch",
)
# -- BOM-path disassembly (no source_stock_entry, no work_order) --
make_stock_entry_test_record(
item_code=scrap_item,
purpose="Material Receipt",
target=wo.fg_warehouse,
qty=50,
basic_rate=10,
)
bom_disassemble_qty = 2
bom_se = frappe.get_doc(
{
"doctype": "Stock Entry",
"stock_entry_type": "Disassemble",
"purpose": "Disassemble",
"from_bom": 1,
"bom_no": bom.name,
"fg_completed_qty": bom_disassemble_qty,
"from_warehouse": wo.fg_warehouse,
"to_warehouse": wo.wip_warehouse,
"company": wo.company,
"posting_date": nowdate(),
"posting_time": nowtime(),
}
)
bom_se.get_items()
bom_se.save()
bom_se.submit()
bom_scrap_row = next((i for i in bom_se.items if i.item_code == scrap_item), None)
self.assertIsNotNone(bom_scrap_row, "Scrap item must appear in BOM-path disassembly")
# v15: BOM scrap_qty=10/FG, no process_loss_per field → qty = 10 * 2 = 20
self.assertEqual(
bom_scrap_row.qty,
20,
f"BOM-path disassembly scrap qty mismatch; expected 20, got {bom_scrap_row.qty}",
)
def test_disassembly_with_additional_rm_not_in_bom(self):
"""
Test that disassembly correctly handles additional raw materials that were
manually added during manufacturing (not part of the BOM).
Test that SE-linked disassembly includes additional raw materials
that were manually added during manufacturing (not part of the BOM).
Scenario:
1. Create Work Order for 10 units with 2 raw materials in BOM
2. Transfer raw materials for manufacture
3. Manufacture in 2 parts (3 units, then 7 units)
4. In each manufacture entry, manually add an extra consumable item
(not in BOM) in proportion to the manufactured qty
5. Create Disassembly for 4 units
6. Verify that the additional RM is included in disassembly with proportional qty
5. Disassemble 3 units linked to first manufacture entry
6. Verify additional RM is included with correct proportional qty from SE1
"""
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
@@ -2655,9 +2716,8 @@ class TestWorkOrder(FrappeTestCase):
se_for_material_transfer.save()
se_for_material_transfer.submit()
# First Manufacture Entry - 3 units
# First Manufacture Entry - 3 units with additional RM
se_manufacture1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
# Additional RM
se_manufacture1.append(
"items",
{
@@ -2670,9 +2730,8 @@ class TestWorkOrder(FrappeTestCase):
se_manufacture1.save()
se_manufacture1.submit()
# Second Manufacture Entry - 7 units
# Second Manufacture Entry - 7 units with additional RM
se_manufacture2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 7))
# AAdditional RM
se_manufacture2.append(
"items",
{
@@ -2688,13 +2747,15 @@ class TestWorkOrder(FrappeTestCase):
wo.reload()
self.assertEqual(wo.produced_qty, 10)
# Disassembly for 4 units
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
# Disassemble 3 units linked to first manufacture entry
disassemble_qty = 3
stock_entry = frappe.get_doc(
make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture1.name)
)
stock_entry.save()
stock_entry.submit()
# No duplicate
# No duplicates
item_counts = {}
for item in stock_entry.items:
item_code = item.item_code
@@ -2707,16 +2768,15 @@ class TestWorkOrder(FrappeTestCase):
f"Found duplicate items in disassembly stock entry: {duplicates}",
)
# Additional RM qty
# Additional RM should be included — qty proportional to SE1 (3 units -> 3 additional RM)
additional_rm_row = next((i for i in stock_entry.items if i.item_code == additional_rm), None)
self.assertIsNotNone(
additional_rm_row,
f"Additional raw material {additional_rm} not found in disassembly",
)
# intentional full reversal as not part of BOM
# eg: dies or consumables used during manufacturing
expected_additional_rm_qty = 3 + 7
# SE1 had 3 additional RM for 3 manufactured units, disassembling all 3
expected_additional_rm_qty = 3
self.assertAlmostEqual(
additional_rm_row.qty,
expected_additional_rm_qty,
@@ -2724,7 +2784,7 @@ class TestWorkOrder(FrappeTestCase):
msg=f"Additional RM qty mismatch: expected {expected_additional_rm_qty}, got {additional_rm_row.qty}",
)
# RM qty
# BOM RM qty — scaled from SE1's rows
for bom_item in bom.items:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
rm_row = next((i for i in stock_entry.items if i.item_code == bom_item.item_code), None)
@@ -2740,6 +2800,7 @@ class TestWorkOrder(FrappeTestCase):
fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertEqual(fg_item_row.qty, disassemble_qty)
# FG + 2 BOM RM + 1 additional RM = 4 items
expected_items = 4
self.assertEqual(
len(stock_entry.items),
@@ -2747,6 +2808,282 @@ class TestWorkOrder(FrappeTestCase):
f"Expected {expected_items} items, found {len(stock_entry.items)}",
)
# Verify traceability
for item in stock_entry.items:
self.assertEqual(item.against_stock_entry, se_manufacture1.name)
self.assertTrue(item.ste_detail)
def test_disassembly_auto_sets_source_stock_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
raw_item = make_item("Test Raw Auto Set Disassembly", {"is_stock_item": 1}).name
fg_item = make_item("Test FG Auto Set Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item], rm_qty=2)
wo = make_wo_order_test_record(production_item=fg_item, qty=5, bom_no=bom.name, status="Not Started")
make_stock_entry_test_record(
item_code=raw_item, purpose="Material Receipt", target=wo.wip_warehouse, qty=50, basic_rate=100
)
se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty))
for item in se_transfer.items:
item.s_warehouse = wo.wip_warehouse
se_transfer.save()
se_transfer.submit()
se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
se_manufacture.submit()
# Disassemble without specifying source_stock_entry
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 3))
stock_entry.save()
# source_stock_entry should be auto-set since only one manufacture entry
self.assertEqual(stock_entry.source_stock_entry, se_manufacture.name)
# All items should have against_stock_entry linked
for item in stock_entry.items:
self.assertEqual(item.against_stock_entry, se_manufacture.name)
self.assertTrue(item.ste_detail)
stock_entry.submit()
def test_disassembly_batch_tracked_items(self):
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
wip_wh = "_Test Warehouse - _TC"
rm_item = make_item(
"Test Batch RM for Disassembly SB",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBRD-RM-.###",
},
).name
fg_item = make_item(
"Test Batch FG for Disassembly SB",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBRD-FG-.###",
},
).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[rm_item], rm_qty=2)
wo = make_wo_order_test_record(
production_item=fg_item,
qty=6,
bom_no=bom.name,
skip_transfer=1,
source_warehouse=wip_wh,
status="Not Started",
)
# Two separate RM receipts → two distinct batches (batch_1, batch_2)
rm_receipt_1 = make_stock_entry_test_record(
item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100
)
rm_batch_1 = get_batch_from_bundle(
frappe.db.get_value(
"Stock Entry Detail",
{"parent": rm_receipt_1.name, "item_code": rm_item},
"serial_and_batch_bundle",
)
)
rm_receipt_2 = make_stock_entry_test_record(
item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100
)
rm_batch_2 = get_batch_from_bundle(
frappe.db.get_value(
"Stock Entry Detail",
{"parent": rm_receipt_2.name, "item_code": rm_item},
"serial_and_batch_bundle",
)
)
self.assertNotEqual(rm_batch_1, rm_batch_2, "Two receipts must create two distinct RM batches")
fg_batch_1 = make_batch(frappe._dict(item=fg_item))
fg_batch_2 = make_batch(frappe._dict(item=fg_item))
# Manufacture entry 1 — 3 FG using batch_1 RM/FG
se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
for row in se_manufacture_1.items:
if row.item_code == rm_item:
row.batch_no = rm_batch_1
row.use_serial_batch_fields = 1
elif row.item_code == fg_item:
row.batch_no = fg_batch_1
row.use_serial_batch_fields = 1
se_manufacture_1.save()
se_manufacture_1.submit()
# Manufacture entry 2 — 3 FG using batch_2 RM/FG
se_manufacture_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
for row in se_manufacture_2.items:
if row.item_code == rm_item:
row.batch_no = rm_batch_2
row.use_serial_batch_fields = 1
elif row.item_code == fg_item:
row.batch_no = fg_batch_2
row.use_serial_batch_fields = 1
se_manufacture_2.save()
se_manufacture_2.submit()
# Disassemble 2 units from SE_1 only — must use SE_1's batches, not SE_2's
disassemble_qty = 2
stock_entry = frappe.get_doc(
make_stock_entry(
wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture_1.name
)
)
stock_entry.save()
stock_entry.submit()
# FG row: must use fg_batch_1 exclusively (fg_batch_2 must not appear)
fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertIsNotNone(fg_row)
self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle")
self.assertEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch_1)
self.assertNotEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch_2)
# RM row: must use rm_batch_1 exclusively (rm_batch_2 must not appear)
rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None)
self.assertIsNotNone(rm_row)
self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle")
self.assertEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch_1)
self.assertNotEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch_2)
# RM qty: 2 FG disassembled x 2 RM per FG = 4
self.assertAlmostEqual(rm_row.qty, 4.0, places=3)
def test_disassembly_serial_tracked_items(self):
from frappe.model.naming import make_autoname
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
wip_wh = "_Test Warehouse - _TC"
rm_item = make_item(
"Test Serial RM for Disassembly SB",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TSRD-RM-.####"},
).name
fg_item = make_item(
"Test Serial FG for Disassembly SB",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TSRD-FG-.####"},
).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[rm_item], rm_qty=2)
wo = make_wo_order_test_record(
production_item=fg_item,
qty=6,
bom_no=bom.name,
skip_transfer=1,
source_warehouse=wip_wh,
status="Not Started",
)
# Two separate RM receipts → two disjoint sets of serial numbers
rm_receipt_1 = make_stock_entry_test_record(
item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100
)
rm_serials_1 = get_serial_nos_from_bundle(
frappe.db.get_value(
"Stock Entry Detail",
{"parent": rm_receipt_1.name, "item_code": rm_item},
"serial_and_batch_bundle",
)
)
self.assertEqual(len(rm_serials_1), 6)
rm_receipt_2 = make_stock_entry_test_record(
item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100
)
rm_serials_2 = get_serial_nos_from_bundle(
frappe.db.get_value(
"Stock Entry Detail",
{"parent": rm_receipt_2.name, "item_code": rm_item},
"serial_and_batch_bundle",
)
)
self.assertEqual(len(rm_serials_2), 6)
self.assertFalse(
set(rm_serials_1) & set(rm_serials_2), "Two receipts must produce disjoint RM serial sets"
)
# Pre-generate two sets of FG serial numbers
series = frappe.db.get_value("Item", fg_item, "serial_no_series")
fg_serials_1 = [make_autoname(series) for _ in range(3)]
fg_serials_2 = [make_autoname(series) for _ in range(3)]
# Manufacture entry 1 — consumes rm_serials_1, produces fg_serials_1
se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
for row in se_manufacture_1.items:
if row.item_code == rm_item:
row.serial_no = "\n".join(rm_serials_1)
row.use_serial_batch_fields = 1
elif row.item_code == fg_item:
row.serial_no = "\n".join(fg_serials_1)
row.use_serial_batch_fields = 1
se_manufacture_1.save()
se_manufacture_1.submit()
# Manufacture entry 2 — consumes rm_serials_2, produces fg_serials_2
se_manufacture_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
for row in se_manufacture_2.items:
if row.item_code == rm_item:
row.serial_no = "\n".join(rm_serials_2)
row.use_serial_batch_fields = 1
elif row.item_code == fg_item:
row.serial_no = "\n".join(fg_serials_2)
row.use_serial_batch_fields = 1
se_manufacture_2.save()
se_manufacture_2.submit()
# Disassemble 2 units from SE_1 only — must use SE_1's serials, not SE_2's
disassemble_qty = 2
stock_entry = frappe.get_doc(
make_stock_entry(
wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture_1.name
)
)
stock_entry.save()
stock_entry.submit()
# FG row: 2 serials consumed — must be subset of fg_serials_1, disjoint from fg_serials_2
fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertIsNotNone(fg_row)
self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle")
fg_dasm_serials = get_serial_nos_from_bundle(fg_row.serial_and_batch_bundle)
self.assertEqual(len(fg_dasm_serials), disassemble_qty)
self.assertTrue(set(fg_dasm_serials).issubset(set(fg_serials_1)))
self.assertFalse(
set(fg_dasm_serials) & set(fg_serials_2), "Disassembly must not use SE_2's FG serials"
)
# RM row: 4 serials returned (2 FG x 2 RM each) — subset of rm_serials_1, disjoint from rm_serials_2
rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None)
self.assertIsNotNone(rm_row)
self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle")
rm_dasm_serials = get_serial_nos_from_bundle(rm_row.serial_and_batch_bundle)
self.assertEqual(len(rm_dasm_serials), disassemble_qty * 2)
self.assertTrue(set(rm_dasm_serials).issubset(set(rm_serials_1)))
self.assertFalse(
set(rm_dasm_serials) & set(rm_serials_2), "Disassembly must not use SE_2's RM serials"
)
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)

View File

@@ -243,6 +243,20 @@ frappe.ui.form.on("Work Order", {
frm.trigger("add_custom_button_to_return_components");
frm.trigger("allow_alternative_item");
frm.trigger("toggle_items_editable");
},
toggle_items_editable(frm) {
let allow_edit = true;
if (!frm.doc.__onload?.allow_editing_items) allow_edit = false;
frm.set_df_property("required_items", "cannot_delete_rows", !allow_edit);
frm.set_df_property("required_items", "cannot_add_rows", !allow_edit);
const grid = frm.fields_dict["required_items"].grid;
grid.update_docfield_property("item_code", "read_only", !allow_edit);
grid.update_docfield_property("required_qty", "read_only", !allow_edit);
grid.refresh();
},
add_custom_button_to_return_components: function (frm) {
@@ -401,7 +415,7 @@ frappe.ui.form.on("Work Order", {
make_disassembly_order(frm) {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.show_disassembly_prompt(frm)
.then((data) => {
if (flt(data.qty) <= 0) {
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
@@ -411,11 +425,14 @@ frappe.ui.form.on("Work Order", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
source_stock_entry: data.source_stock_entry,
});
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
if (stock_entry) {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
}
});
},
@@ -425,10 +442,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 +463,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 +494,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);
},
@@ -838,6 +882,60 @@ erpnext.work_order = {
return flt(max, precision("qty"));
},
show_disassembly_prompt: function (frm) {
let max_qty = flt(frm.doc.produced_qty - frm.doc.disassembled_qty);
let fields = [
{
fieldtype: "Link",
label: __("Source Manufacture Entry"),
fieldname: "source_stock_entry",
options: "Stock Entry",
description: __("Optional. Select a specific manufacture entry to reverse."),
get_query: () => {
return {
filters: {
work_order: frm.doc.name,
purpose: "Manufacture",
docstatus: 1,
},
};
},
onchange: async function () {
if (!frm.disassembly_prompt) return;
let se_name = this.value;
let qty = max_qty;
if (se_name) {
qty = await frappe.xcall(
"erpnext.manufacturing.doctype.work_order.work_order.get_disassembly_available_qty",
{ stock_entry_name: se_name }
);
}
frm.disassembly_prompt.set_value("qty", qty);
frm.disassembly_prompt.fields_dict.qty.set_description(__("Max: {0}", [qty]));
},
},
{
fieldtype: "Float",
label: __("Qty for {0}", [__("Disassemble")]),
fieldname: "qty",
description: __("Max: {0}", [max_qty]),
default: max_qty,
},
];
return new Promise((resolve, reject) => {
frm.disassembly_prompt = frappe.prompt(
fields,
(data) => resolve(data),
__("Disassemble"),
__("Create")
);
});
},
show_prompt_for_qty_input: function (frm, purpose) {
let max = this.get_max_transferable_qty(frm, purpose);

View File

@@ -141,6 +141,7 @@ class WorkOrder(Document):
def onload(self):
ms = frappe.get_doc("Manufacturing Settings")
self.set_onload("allow_editing_items", ms.allow_editing_of_items_and_quantities_in_work_order)
self.set_onload("material_consumption", ms.material_consumption)
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
@@ -167,7 +168,11 @@ class WorkOrder(Document):
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))
if not len(self.get("required_items")) or not frappe.db.get_single_value(
"Manufacturing Settings", "allow_editing_of_items_and_quantities_in_work_order"
):
self.set_required_items(reset_only_qty=len(self.get("required_items")))
self.validate_operations_sequence()
def validate_operations_sequence(self):
@@ -220,39 +225,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):
@@ -370,7 +388,7 @@ class WorkOrder(Document):
if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
if status != "Stopped":
if status not in ["Closed", "Stopped"]:
status = "Not Started"
if flt(self.material_transferred_for_manufacturing) > 0:
status = "In Process"
@@ -426,7 +444,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:
@@ -818,7 +836,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
@@ -1359,7 +1377,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
@frappe.whitelist()
def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None):
def make_work_order(
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
):
from erpnext import get_default_company
if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
@@ -1374,6 +1396,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.company = company or get_default_company()
wo_doc.update(item_details)
wo_doc.bom_no = bom_no
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)
@@ -1462,7 +1485,13 @@ def set_work_order_ops(name):
@frappe.whitelist()
def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
def make_stock_entry(
work_order_id: str,
purpose: str,
qty: float | None = None,
target_warehouse: str | None = None,
source_stock_entry: str | None = None,
):
work_order = frappe.get_doc("Work Order", work_order_id)
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
wip_warehouse = work_order.wip_warehouse
@@ -1499,6 +1528,8 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
if purpose == "Disassemble":
stock_entry.from_warehouse = work_order.fg_warehouse
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
if source_stock_entry:
stock_entry.source_stock_entry = source_stock_entry
stock_entry.set_stock_entry_type()
stock_entry.get_items()
@@ -1509,6 +1540,28 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
return stock_entry.as_dict()
@frappe.whitelist()
def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float:
se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True)
if not se:
return 0.0
filters = {
"source_stock_entry": stock_entry_name,
"purpose": "Disassemble",
"docstatus": 1,
}
if current_se_name:
filters["name"] = ("!=", current_se_name)
already_disassembled = flt(
frappe.db.get_value("Stock Entry", filters, "sum(fg_completed_qty)", order_by=None)
)
return flt(se.fg_completed_qty) - already_disassembled
@frappe.whitelist()
def get_default_warehouse():
doc = frappe.get_cached_doc("Manufacturing Settings")

View File

@@ -151,7 +151,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-11-19 15:48:16.823384",
"modified": "2025-12-02 11:16:05.081613",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",

View File

@@ -79,9 +79,6 @@ class Workstation(Document):
self.total_working_hours += row.hours
def validate_working_hours(self, row):
if not (row.start_time and row.end_time):
frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
if get_time(row.start_time) >= get_time(row.end_time):
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))

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
@@ -432,3 +431,4 @@ 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

View File

@@ -0,0 +1,33 @@
import frappe
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
def execute():
PurchaseOrderItem = DocType("Purchase Order Item")
MaterialRequestItem = DocType("Material Request Item")
poi_query = (
frappe.qb.from_(PurchaseOrderItem)
.select(PurchaseOrderItem.sales_order_item, Sum(PurchaseOrderItem.stock_qty))
.where(PurchaseOrderItem.sales_order_item.isnotnull() & PurchaseOrderItem.docstatus == 1)
.groupby(PurchaseOrderItem.sales_order_item)
)
mri_query = (
frappe.qb.from_(MaterialRequestItem)
.select(MaterialRequestItem.sales_order_item, Sum(MaterialRequestItem.stock_qty))
.where(MaterialRequestItem.sales_order_item.isnotnull() & MaterialRequestItem.docstatus == 1)
.groupby(MaterialRequestItem.sales_order_item)
)
poi_data = poi_query.run()
mri_data = mri_query.run()
updates_against_poi = {data[0]: {"ordered_qty": data[1]} for data in poi_data}
updates_against_mri = {data[0]: {"requested_qty": data[1], "ordered_qty": 0} for data in mri_data}
frappe.db.auto_commit_on_many_writes = 1
frappe.db.bulk_update("Sales Order Item", updates_against_mri)
frappe.db.bulk_update("Sales Order Item", updates_against_poi)
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -47,21 +47,8 @@ def create_customer_or_supplier():
if party_exists(doctype, user):
return
party = frappe.new_doc(doctype)
fullname = frappe.utils.get_fullname(user)
if not doctype == "Customer":
party.update(
{
"supplier_name": fullname,
"supplier_group": "All Supplier Groups",
"supplier_type": "Individual",
}
)
party.flags.ignore_mandatory = True
party.insert(ignore_permissions=True)
party = create_party(doctype, fullname)
alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier"
if party_exists(alternate_doctype, user):
@@ -69,6 +56,22 @@ def create_customer_or_supplier():
fullname += "-" + doctype
create_party_contact(doctype, fullname, user, party.name)
return party
def create_party(doctype, fullname):
party = frappe.new_doc(doctype)
# Can't set parent party as group
party.update(
{
f"{doctype.lower()}_name": fullname,
f"{doctype.lower()}_type": "Individual",
}
)
party.flags.ignore_mandatory = True
party.insert(ignore_permissions=True)
return party

View File

@@ -8,6 +8,7 @@ import frappe
from frappe.tests.utils import change_settings
from frappe.utils import add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice
from erpnext.setup.doctype.employee.test_employee import make_employee
@@ -202,6 +203,58 @@ class TestTimesheet(unittest.TestCase):
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def test_partial_billing_and_return(self):
"""
Test Timesheet status transitions during partial billing, full billing,
sales return, and return cancellation.
Scenario:
1. Create a Timesheet with two billable time logs.
2. Create a Sales Invoice billing only one time log → Timesheet becomes Partially Billed.
3. Create another Sales Invoice billing the remaining time log → Timesheet becomes Billed.
4. Create a Sales Return against the second invoice → Timesheet reverts to Partially Billed.
5. Cancel the Sales Return → Timesheet returns to Billed status.
This test ensures Timesheet status is recalculated correctly
across billing and return lifecycle events.
"""
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1, do_not_submit=True)
timesheet_detail = timesheet.append("time_logs", {})
timesheet_detail.is_billable = 1
timesheet_detail.activity_type = "_Test Activity Type"
timesheet_detail.from_time = timesheet.time_logs[0].to_time + datetime.timedelta(minutes=1)
timesheet_detail.hours = 2
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(
hours=timesheet_detail.hours
)
timesheet.save().submit()
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice.due_date = nowdate()
sales_invoice.timesheets.pop()
sales_invoice.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_invoice2 = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice2.due_date = nowdate()
sales_invoice2.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Billed")
sales_return = make_sales_return(sales_invoice2.name).submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_return.load_from_db()
sales_return.cancel()
timesheet.load_from_db()
self.assertEqual(timesheet.time_logs[1].sales_invoice, sales_invoice2.name)
self.assertEqual(timesheet.status, "Billed")
def make_timesheet(
employee,
@@ -211,6 +264,7 @@ def make_timesheet(
project=None,
task=None,
company=None,
do_not_submit=False,
):
update_activity_type(activity_type)
timesheet = frappe.new_doc("Timesheet")
@@ -237,7 +291,8 @@ def make_timesheet(
else:
timesheet.save(ignore_permissions=True)
timesheet.submit()
if not do_not_submit:
timesheet.submit()
return timesheet

View File

@@ -260,6 +260,33 @@ frappe.ui.form.on("Timesheet", {
parent_project: function (frm) {
set_project_in_timelog(frm);
},
employee: function (frm) {
if (frm.doc.employee && frm.doc.time_logs) {
const selected_employee = frm.doc.employee;
frm.doc.time_logs.forEach((row) => {
if (row.activity_type) {
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
employee: frm.doc.employee,
activity_type: row.activity_type,
currency: frm.doc.currency,
},
callback: function (r) {
if (r.message) {
if (selected_employee !== frm.doc.employee) return;
row.billing_rate = r.message["billing_rate"];
row.costing_rate = r.message["costing_rate"];
frm.refresh_fields("time_logs");
calculate_billing_costing_amount(frm, row.doctype, row.name);
}
},
});
}
});
}
},
});
frappe.ui.form.on("Timesheet Detail", {

View File

@@ -91,7 +91,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled",
"options": "Draft\nSubmitted\nPartially Billed\nBilled\nPayslip\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1
},
@@ -310,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-20 15:59:11.107831",
"modified": "2026-04-06 22:30:28.513139",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

View File

@@ -50,7 +50,9 @@ class Timesheet(Document):
per_billed: DF.Percent
sales_invoice: DF.Link | None
start_date: DF.Date | None
status: DF.Literal["Draft", "Submitted", "Billed", "Payslip", "Completed", "Cancelled"]
status: DF.Literal[
"Draft", "Submitted", "Partially Billed", "Billed", "Payslip", "Completed", "Cancelled"
]
time_logs: DF.Table[TimesheetDetail]
title: DF.Data | None
total_billable_amount: DF.Currency
@@ -126,6 +128,9 @@ class Timesheet(Document):
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
self.status = "Billed"
if 0.0 < flt(self.per_billed, self.precision("per_billed")) < 100.0:
self.status = "Partially Billed"
if self.sales_invoice:
self.status = "Completed"
@@ -423,7 +428,9 @@ def get_timesheet_data(name, project):
@frappe.whitelist()
def make_sales_invoice(source_name, item_code=None, customer=None, currency=None):
def make_sales_invoice(
source_name: str, item_code: str | None = None, customer: str | None = None, currency: str | None = None
):
target = frappe.new_doc("Sales Invoice")
timesheet = frappe.get_doc("Timesheet", source_name)
@@ -452,7 +459,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate})
for time_log in timesheet.time_logs:
if time_log.is_billable:
if time_log.is_billable and not time_log.sales_invoice:
target.append(
"timesheets",
{

View File

@@ -1,6 +1,9 @@
frappe.listview_settings["Timesheet"] = {
add_fields: ["status", "total_hours", "start_date", "end_date"],
get_indicator: function (doc) {
if (doc.status == "Partially Billed") {
return [__("Partially Billed"), "orange", "status,=," + "Partially Billed"];
}
if (doc.status == "Billed") {
return [__("Billed"), "green", "status,=," + "Billed"];
}

View File

@@ -140,6 +140,7 @@ erpnext.buying = {
this.toggle_subcontracting_fields();
super.refresh();
this.prevent_past_schedule_dates(this.frm);
}
toggle_subcontracting_fields() {
@@ -183,6 +184,28 @@ erpnext.buying = {
erpnext.utils.set_letter_head(this.frm)
}
schedule_date(doc, cdt, cdn) {
if (doc.schedule_date && !cdt.endsWith(" Item")) {
doc.items.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "schedule_date", doc.schedule_date);
});
}
}
transaction_date() {
super.transaction_date();
this.frm.set_value("schedule_date", "");
this.prevent_past_schedule_dates(this.frm);
}
prevent_past_schedule_dates(frm) {
if (frm.doc.transaction_date && frm.fields_dict["schedule_date"]) {
frm.fields_dict["schedule_date"].datepicker?.update({
minDate: new Date(frm.doc.transaction_date),
});
}
}
supplier_address() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");

View File

@@ -173,9 +173,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (!tax.dont_recompute_tax) {
tax.item_wise_tax_detail = {};
}
var tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"];
var tax_fields = [
"net_amount",
"total",
"tax_amount_after_discount_amount",
"tax_amount_for_current_item",
"grand_total_for_current_item",
"tax_fraction_for_current_item",
"grand_total_fraction_for_current_item",
];
if (cstr(tax.charge_type) != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
@@ -363,9 +369,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(doc.taxes, function(i, tax) {
// tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
var [current_net_amount, current_tax_amount] = me.get_current_tax_amount(
item,
tax,
item_tax_map
);
if (frappe.flags.round_row_wise_tax) {
current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
current_net_amount = flt(current_net_amount, precision("net_amount", tax));
}
// Adjust divisional loss to the last item
@@ -380,6 +391,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (tax.charge_type != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
tax.tax_amount += current_tax_amount;
tax.net_amount += current_net_amount;
}
// store tax_amount for current item as it will be used for
@@ -430,8 +442,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
for (const [i, tax] of doc.taxes.entries()) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]);
me.round_off_base_values(tax);
@@ -464,6 +475,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
get_current_tax_amount(item, tax, item_tax_map) {
var tax_rate = this._get_tax_rate(tax, item_tax_map);
var current_tax_amount = 0.0;
var current_net_amount = 0.0;
// To set row_id by default as previous row.
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
@@ -476,21 +488,27 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
}
if(tax.charge_type == "Actual") {
current_net_amount = item.net_amount
// distribute the tax amount proportionally to each item row
var actual = flt(tax.tax_amount, precision("tax_amount", tax));
current_tax_amount = this.frm.doc.net_total ?
((item.net_amount / this.frm.doc.net_total) * actual) : 0.0;
} else if(tax.charge_type == "On Net Total") {
if (tax.account_head in item_tax_map) {
current_net_amount = item.net_amount
};
current_tax_amount = (tax_rate / 100.0) * item.net_amount;
} else if(tax.charge_type == "On Previous Row Amount") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item
current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item;
} else if(tax.charge_type == "On Previous Row Total") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item
current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item;
} else if (tax.charge_type == "On Item Quantity") {
// don't sum current net amount due to the field being a currency field
current_tax_amount = tax_rate * item.qty;
}
@@ -498,7 +516,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
}
return current_tax_amount;
return [current_net_amount, current_tax_amount];
}
set_item_wise_tax(item, tax, tax_rate, current_tax_amount) {
@@ -532,7 +550,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, precision("tax_amount", tax));
tax.net_amount = flt(tax.net_amount, precision("net_amount", tax));
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount,
precision("tax_amount", tax)
);
}
round_off_base_values(tax) {

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