Compare commits

..

418 Commits

Author SHA1 Message Date
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
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
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
Frappe PR Bot
1ee03f41f2 chore(release): Bumped to Version 15.100.2
## [15.100.2](https://github.com/frappe/erpnext/compare/v15.100.1...v15.100.2) (2026-03-06)

### Bug Fixes

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

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

(cherry picked from commit 1d3d09f48c)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py

* chore: resolve conflicts

* chore: resolve conflicts

---------

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

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


(cherry picked from commit 702adda000)

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

### Bug Fixes

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

### Bug Fixes

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

### Features

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

### Performance Improvements

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

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

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

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

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

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

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

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

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

### Bug Fixes

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

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

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

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

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

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

* Update purchase_register.js

* Update purchase_register.py

(cherry picked from commit 9ea963bfe9)

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

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

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

### Bug Fixes

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

### Features

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

### Reverts

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

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

* test: validate quotation status

* chore: remove submit

(cherry picked from commit d638f3e033)

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

* test: validate quotation status

* chore: remove submit

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

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

* fix: remove delete function call for work order

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

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

* fix: take care of existing property setters

* fix: cancelling Bank Transactions should still be possible

* fix: handle blank options in patch

* fix: hide Reference Due Date for Bank Transaction

(cherry picked from commit 387fb1b202)

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

* chore: resolve conflicts

---------

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

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

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

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

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

* feat: update item button addition for quotation

* feat: update item button addition for supplier quotation

* fix: test case

---------

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

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

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

* feat: update item button addition for quotation

* feat: update item button addition for supplier quotation

* fix: test case

---------

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

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

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-02-19 14:41:01 +00:00
Mihir Kandoi
283b118773 Merge pull request #52805 from frappe/mergify/bp/version-15-hotfix/pr-52804
fix(Purchase Receipt): copy project from first row when adding items (backport #52804)
2026-02-19 14:03:34 +05:30
Marc Ramser
fd48fb49b9 fix(Purchase Receipt): copy project from first row when adding items
Adds `items_add` method to copy expense_account, cost_center and project from first row to newly added items, matching Purchase Invoice behavior.

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

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

### Bug Fixes

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

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

This fixes the comparison.

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

### Bug Fixes

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

### Features

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

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

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

* refactor: remove non-existent field

---------

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

* fix(pos_invoice): add correct depends on condition

* fix: show field in sales order

* refactor: eval condition

(cherry picked from commit 219cf6bc57)

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

* chore: resolve conflict

---------

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

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

# Conflicts:
#	erpnext/crm/doctype/market_segment/market_segment.json
2026-02-11 08:59:24 +00:00
Dharanidharan S
8bdbb24d73 fix(accounts): correct base grand total and rounded total mismatch (#51739)
(cherry picked from commit d82c92a237)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2026-02-10 19:05:23 +05:30
ljain112
e57f3fe727 test: update expected values for tax withholding calculations in tests 2026-02-10 14:40:53 +05:30
ljain112
68099a9b5c fix: set base_tax_withholding_net_total for jv in tds report 2026-02-10 13:53:31 +05:30
ljain112
ed42d54989 fix: add base_tax_withholding_net_total to tax withholding report 2026-02-10 13:30:12 +05:30
ljain112
b740846b68 refactor: update labels for tax withholding reports columns to improve clarity 2026-02-10 13:08:35 +05:30
Navin-S-R
273029d0f0 fix: recalculate tax withholding during Purchase Order child update 2026-01-28 18:51:49 +05:30
208 changed files with 5166 additions and 1530 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.97.0"
__version__ = "15.103.1"
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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -1101,7 +1101,6 @@ def create_pos_invoice(**args):
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name

View File

@@ -146,6 +146,7 @@ class POSInvoiceMergeLog(Document):
sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.update_stock = 1
if not sales_invoice.posting_date:
sales_invoice.posting_date = getdate(self.posting_date)
@@ -174,6 +175,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.update_stock = 1
credit_note.posting_date = getdate(self.posting_date)
credit_note.posting_time = get_time(self.posting_time)
# TODO: return could be against multiple sales invoice which could also have been consolidated?

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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):
@@ -1729,10 +1731,6 @@ class PurchaseInvoice(BuyingController):
project_doc.db_update()
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
if self.bill_no:
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)

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",

View File

@@ -804,11 +804,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:
@@ -854,9 +852,6 @@ class SalesInvoice(SellingController):
if selling_price_list:
self.set("selling_price_list", selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
@@ -1097,7 +1092,9 @@ class SalesInvoice(SellingController):
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
def update_packing_list(self):
if cint(self.update_stock) == 1:
if self.doctype == "POS Invoice" or (
self.doctype == "Sales Invoice" and cint(self.update_stock) == 1
):
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
@@ -1200,6 +1197,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:
@@ -2516,7 +2516,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
},
doctype + " Item": item_field_map,
},
@@ -2745,6 +2745,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")]
@@ -2766,6 +2768,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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

@@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase):
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
sinv = sinv.save().submit()
filters = frappe._dict(

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

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

View File

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

View File

@@ -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 = 0, 0, 0, 0
tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0
tax_withholding_category, rate = None, None
bill_no, bill_date = "", ""
party = entry.party or entry.against
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,29 +69,35 @@ 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
else:
if tax_amount and rate:
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
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":
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
base_tax_withholding_net_total = total_amount
if tax_amount:
if party_map.get(party, {}).get("party_type") == "Supplier":
@@ -125,6 +124,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
"rate": rate,
"total_amount": total_amount,
"grand_total": grand_total,
"base_tax_withholding_net_total": base_tax_withholding_net_total,
"base_total": base_total,
"tax_amount": tax_amount,
"transaction_date": posting_date,
@@ -141,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"]
@@ -158,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
@@ -167,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
@@ -252,14 +266,14 @@ def get_columns(filters):
"width": 60,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"label": _("Tax Withholding Net Total"),
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Float",
"width": 120,
"width": 150,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
@@ -270,10 +284,16 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Grand Total"),
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Float",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 120,
"width": 170,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
{
@@ -296,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(
@@ -322,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,
)
@@ -361,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"):
@@ -391,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": [
@@ -419,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

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

View File

@@ -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
@@ -128,7 +124,7 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Total Amount"),
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,

View File

@@ -345,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
def calculate_total_row(data, company_currency, show_group_accounts=True):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
@@ -362,10 +362,16 @@ def calculate_total_row(accounts, company_currency):
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
def sum_value_fields(row):
for field in value_fields:
total_row[field] += row[field]
for d in data:
if not show_group_accounts:
sum_value_fields(d)
elif show_group_accounts and not d.get("parent_account"):
sum_value_fields(d)
return total_row
@@ -409,11 +415,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
row["has_value"] = has_value
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
if not filters.get("show_group_accounts"):
data = hide_group_accounts(data)
total_row = calculate_total_row(
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
)
data.extend([{}, total_row])
return data

View File

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

View File

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

@@ -196,6 +196,9 @@ class PurchaseOrder(BuyingController):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
if self.is_subcontracted:
self.status_updater[0]["source_field"] = "fg_item_qty"
def validate(self):
super().validate()
@@ -823,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
@@ -874,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},
@@ -909,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:
@@ -936,7 +935,14 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
if doctype == "Packed Item":
if key is None:
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
key = frappe.get_cached_value(
"Packed Item",
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
field,
)
if key is None:
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
if row.voucher_type == "Delivery Note":
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
elif row.voucher_type == "Sales Invoice":

View File

@@ -318,9 +318,10 @@ class SellingController(StockController):
if is_internal_customer or not is_stock_item:
continue
if item.get("incoming_rate") and item.base_net_rate < (
rate_field = "valuation_rate" if self.doctype in ["Sales Order", "Quotation"] else "incoming_rate"
if item.get(rate_field) and item.base_net_rate < (
valuation_rate := flt(
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
item.get(rate_field) * (item.conversion_factor or 1), item.precision("base_net_rate")
)
):
throw_message(
@@ -483,10 +484,37 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self):
def reset_incoming_rate():
old_item = next(
(
item
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
if item.name == d.name
),
None,
)
if old_item:
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
if (
old_item.item_code != d.item_code
or old_item.warehouse != d.warehouse
or old_qty != qty
or old_item.serial_no != d.serial_no
or get_serial_nos(old_item.serial_and_batch_bundle)
!= get_serial_nos(d.serial_and_batch_bundle)
or old_item.batch_no != d.batch_no
or get_batch_nos(old_item.serial_and_batch_bundle)
!= get_batch_nos(d.serial_and_batch_bundle)
):
d.incoming_rate = 0
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
from erpnext.stock.serial_batch_bundle import get_batch_nos
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(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
@@ -495,6 +523,8 @@ class SellingController(StockController):
"Selling Settings", "set_zero_rate_for_expired_batch"
)
is_standalone = self.is_return and not self.return_against
old_doc = self.get_doc_before_save()
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
@@ -526,27 +556,7 @@ class SellingController(StockController):
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
if old_doc:
old_item = next(
(
item
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
if item.name == d.name
),
None,
)
if old_item:
old_qty = flt(
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
)
if (
old_item.item_code != d.item_code
or old_item.warehouse != d.warehouse
or old_qty != qty
or old_item.batch_no != d.batch_no
or get_batch_nos(old_item.serial_and_batch_bundle)
!= get_batch_nos(d.serial_and_batch_bundle)
):
d.incoming_rate = 0
reset_incoming_rate()
if (
not d.incoming_rate
@@ -565,11 +575,12 @@ class SellingController(StockController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": d.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
"allow_zero_valuation": d.get("allow_zero_valuation_rate"),
"batch_no": d.batch_no,
"serial_no": d.serial_no,
},
raise_error_if_no_rate=False,
raise_error_if_no_rate=is_standalone,
fallbacks=not is_standalone,
)
if (

View File

@@ -111,7 +111,7 @@ status_map = {
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
[
"Ordered",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
],
[
"Transferred",
@@ -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

@@ -57,6 +57,8 @@ class StockController(AccountsController):
if not self.get("is_return"):
self.validate_inspection()
self.validate_warehouse_of_sabb()
self.validate_serialized_batch()
self.clean_serial_nos()
self.validate_customer_provided_item()
@@ -65,6 +67,45 @@ class StockController(AccountsController):
self.validate_putaway_capacity()
self.reset_conversion_factor()
def validate_warehouse_of_sabb(self):
if self.is_internal_transfer():
return
doc_before_save = self.get_doc_before_save()
for row in self.items:
if not row.get("serial_and_batch_bundle"):
continue
sabb_details = frappe.db.get_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,
["type_of_transaction", "warehouse", "has_serial_no"],
as_dict=True,
)
if not sabb_details:
continue
if sabb_details.type_of_transaction != "Outward":
continue
warehouse = row.get("warehouse") or row.get("s_warehouse")
if sabb_details.warehouse != warehouse:
frappe.throw(
_(
"Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}."
).format(row.idx, warehouse, sabb_details.warehouse, row.serial_and_batch_bundle)
)
if self.doctype == "Stock Reconciliation":
continue
if sabb_details.has_serial_no and doc_before_save and doc_before_save.get("items"):
prev_row = doc_before_save.get("items", {"idx": row.idx})
if prev_row and prev_row[0].serial_and_batch_bundle != row.serial_and_batch_bundle:
sabb_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
sabb_doc.validate_serial_no_status()
def reset_conversion_factor(self):
for row in self.get("items"):
if row.uom != row.stock_uom:
@@ -1094,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)
@@ -1101,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)
@@ -1591,7 +1632,7 @@ def get_gl_entries_for_preview(doctype, docname, fields):
def get_columns(raw_columns, fields):
return [
{"name": d.get("label"), "editable": False, "width": 110}
{"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")}
for d in raw_columns
if not d.get("hidden") and d.get("fieldname") in fields
]
@@ -1661,7 +1702,7 @@ def check_item_quality_inspection(doctype, items):
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
def make_quality_inspections(company, doctype, docname, items):
if isinstance(items, str):
items = json.loads(items)
@@ -1680,6 +1721,7 @@ def make_quality_inspections(doctype, docname, items):
quality_inspection = frappe.get_doc(
{
"company": company,
"doctype": "Quality Inspection",
"inspection_type": "Incoming",
"inspected_by": frappe.session.user,

View File

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

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

View File

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

View File

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

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

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