Compare commits

..

160 Commits

Author SHA1 Message Date
Frappe PR Bot
cc0a895760 chore(release): Bumped to Version 15.82.2
## [15.82.2](https://github.com/frappe/erpnext/compare/v15.82.1...v15.82.2) (2025-10-09)

### Bug Fixes

* Reset Raw Materials Table button not working ([00db37e](00db37e306))
* sales return for product bundle items ([2ea583e](2ea583ee16))
2025-10-09 12:31:16 +00:00
rohitwaghchaure
6ec370ad86 Merge pull request #49989 from frappe/mergify/bp/version-15/pr-49974
fix: Reset Raw Materials Table button not working (backport #49973) (backport #49974)
2025-10-09 17:59:42 +05:30
rohitwaghchaure
994f992fcf Merge pull request #49990 from frappe/mergify/bp/version-15/pr-49984
fix: sales return for product bundle items (backport #49975) (backport #49984)
2025-10-09 17:59:28 +05:30
rohitwaghchaure
3e816f6ea1 chore: fix conflicts
(cherry picked from commit 1fbc03c104)
2025-10-09 12:09:21 +00:00
Rohit Waghchaure
789cea9a85 test: test case for sales return for product bundle
(cherry picked from commit 1d57bbca11)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
(cherry picked from commit 6ba55bbee0)
2025-10-09 12:09:21 +00:00
Rohit Waghchaure
2ea583ee16 fix: sales return for product bundle items
(cherry picked from commit 13ce7279a8)
(cherry picked from commit ac46b3d1ca)
2025-10-09 12:09:20 +00:00
Rohit Waghchaure
00db37e306 fix: Reset Raw Materials Table button not working
(cherry picked from commit 128e243945)
(cherry picked from commit 81ed32ff51)
2025-10-09 12:06:21 +00:00
Frappe PR Bot
c84952169f chore(release): Bumped to Version 15.82.1
## [15.82.1](https://github.com/frappe/erpnext/compare/v15.82.0...v15.82.1) (2025-10-08)

### Bug Fixes

* batch qty for expired batches ([4e2a42d](4e2a42d092))
* incorrect field valuation_rate ([9c3f4b6](9c3f4b69af))
2025-10-08 14:32:21 +00:00
rohitwaghchaure
d103409077 Merge pull request #49972 from frappe/mergify/bp/version-15/pr-49971
fix: batch qty for expired batches (backport #49967) (backport #49971)
2025-10-08 20:00:34 +05:30
rohitwaghchaure
d1dd3ca097 Merge pull request #49970 from frappe/mergify/bp/version-15/pr-49968
fix: incorrect field valuation_rate (backport #49966) (backport #49968)
2025-10-08 20:00:19 +05:30
Rohit Waghchaure
4e2a42d092 fix: batch qty for expired batches
(cherry picked from commit ff2faf36a7)
(cherry picked from commit f4816e4960)
2025-10-08 14:12:42 +00:00
Rohit Waghchaure
9c3f4b69af fix: incorrect field valuation_rate
(cherry picked from commit 630d873214)
(cherry picked from commit 93df11a0cf)
2025-10-08 13:38:13 +00:00
Frappe PR Bot
e2e1ac0dba chore(release): Bumped to Version 15.82.0
# [15.82.0](https://github.com/frappe/erpnext/compare/v15.81.3...v15.82.0) (2025-10-07)

### Bug Fixes

* **accounting:** ensure proper removal of advance references during unreconcillation ([825ccd3](825ccd3422))
* add default scrap warehouse in wo ([a5ed9fd](a5ed9fdc67))
* Add try-finally for setting buying price list ([e21baec](e21baec246))
* check is_rejected attribute ([bdf150b](bdf150bdf8))
* clear asset custodian  when asset take back from employee without assign to another employee ([f4cdb49](f4cdb49126))
* **Common Code:** fetch canonical URI from Code List (backport [#49882](https://github.com/frappe/erpnext/issues/49882)) ([#49884](https://github.com/frappe/erpnext/issues/49884)) ([2c0501b](2c0501b05f))
* delete column dynamically based on the naming by ([040873a](040873a442))
* do not consider draft bundles ([75323fd](75323fda01))
* do not fetch disabled item tax template ([c42dcbe](c42dcbe739))
* exclude opening entries ([ed1c270](ed1c270398))
* failing patch ([0dad195](0dad1957c8))
* get unconsumed qty as per BOM qty ([82386b1](82386b18aa))
* linter; dont change doc after DB update ([#49907](https://github.com/frappe/erpnext/issues/49907)) ([6c47353](6c47353205))
* **manufacturing:** prevent KeyError in BOM Creator when sub-assembly reused ([07c3755](07c3755f31))
* optimize SQL query by adding index on batch ([a83331b](a83331bd2f))
* **profit and loss statement:** incorrect total calculation ([e132c45](e132c457f2))
* remove allow_on_submit for pick list items ([4ccdede](4ccdedeb12))
* resolved conflict ([a39bc62](a39bc626c7))
* retain shipping address in doc ([20c21a4](20c21a4dc0))
* set fg_completed_qty based upon fg item qty ([b6d57ff](b6d57ff8a5))
* Set paid amount automatically only if return entry validated and has negative grand total ([#49829](https://github.com/frappe/erpnext/issues/49829)) ([bd3503a](bd3503a3d8))
* SQL operator precedence in Project query customer filter ([d950de2](d950de2d09))
* **subscription:** include days before ([f27b754](f27b754570))
* too many writes on patch run ([a2bf53f](a2bf53ff0a))
* use item valuation rate if no bin ([a4b5a74](a4b5a74644))
* validate transfer_qty based on overproduction wo percentage ([7ce97ce](7ce97ce0c2))
* warning message if the batch has incorrect qty ([3f3fd20](3f3fd20b31))

### Features

* allow fallback to default selling price list (backport [#49634](https://github.com/frappe/erpnext/issues/49634)) ([#49704](https://github.com/frappe/erpnext/issues/49704)) ([95387b4](95387b4bf0))
* dynamic due date in payment terms when fetched from order (backport [#48864](https://github.com/frappe/erpnext/issues/48864)) ([#49938](https://github.com/frappe/erpnext/issues/49938)) ([baa6d2b](baa6d2bcdc))
* recalculate batch qty ([d49b64d](d49b64dc7c))
* validating asset scrap date ([#43093](https://github.com/frappe/erpnext/issues/43093)) ([0eb76f4](0eb76f4d2c))

### Performance Improvements

* serial nos / batches reposting ([8a310ef](8a310efc97))
2025-10-07 13:23:00 +00:00
ruthra kumar
b557a03ba7 Merge pull request #49942 from frappe/version-15-hotfix
chore: release v15
2025-10-07 18:51:39 +05:30
mergify[bot]
11eab0c852 Merge pull request #49946 from frappe/mergify/bp/version-15-hotfix/pr-49721
feat: add company links to Email Account and Communication (backport #49721)
2025-10-07 17:01:37 +05:30
rohitwaghchaure
8683f49e8b Merge pull request #49945 from frappe/mergify/bp/version-15-hotfix/pr-49935
refactor: old serial nos filter (backport #49935)
2025-10-07 16:29:16 +05:30
rohitwaghchaure
6ea07ba56d chore: fix conflicts 2025-10-07 16:02:30 +05:30
Rohit Waghchaure
e02a55b188 refactor: old serial nos filter
(cherry picked from commit 6a8bd0ae9e)

# Conflicts:
#	erpnext/stock/deprecated_serial_batch.py
2025-10-07 09:52:27 +00:00
mergify[bot]
baa6d2bcdc feat: dynamic due date in payment terms when fetched from order (backport #48864) (#49938)
* feat: dynamic due date in payment terms when fetched from order (#48864)

* fix: dynamic due date when payment terms are fetched from order

* fix(test): use change_settings decorator for settings enable and disable

* fix(test): compare schedule for due_date dynamically

* fix: save conditions for due date at invoice level

* fix: make fields read only and on change of date unset the date condition fields

* fix: remove fetch_form

* fix: correct field assingment

* fix: revert unwanted changes

* refactor: streamline payment term field assignments and enhance discount date handling

* refactor: remove payment_term from fields_to_copy and optimize currency handling in transaction callback

* refactor: ensure default values for payment schedule and discount validity fields

(cherry picked from commit 3c70cbbaf8)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: Lakshit Jain <ljain112@gmail.com>
2025-10-07 14:10:15 +05:30
rohitwaghchaure
631ffd55ef Merge pull request #49934 from frappe/mergify/bp/version-15-hotfix/pr-49846
chore(Stock Qty vs Serial No Count): add show_disabled_items filter (backport #49846)
2025-10-07 11:49:20 +05:30
rethik
7a457dafe0 chore: add show_disabled_items filter to show both enabled and disabled items
(cherry picked from commit bf5f24c0e0)
2025-10-07 05:59:36 +00:00
rohitwaghchaure
94a5867303 Merge pull request #49933 from frappe/mergify/bp/version-15-hotfix/pr-49762
fix(manufacturing): prevent KeyError in BOM Creator when sub-assembly reused (backport #49762)
2025-10-07 11:23:12 +05:30
rohitwaghchaure
1663b875c0 Merge pull request #49932 from frappe/mergify/bp/version-15-hotfix/pr-49702
fix: do not fetch disabled item tax template (backport #49702)
2025-10-07 11:22:57 +05:30
ljain112
d47f3cc101 chore: resolve conflicts 2025-10-07 11:01:47 +05:30
KerollesFathy
07c3755f31 fix(manufacturing): prevent KeyError in BOM Creator when sub-assembly reused
Ensure missing (fg_item, fg_reference_id) keys are initialized in
production_item_wise_rm before appending items. This avoids crashes
when the same sub-assembly is referenced under multiple parents.

(cherry picked from commit 4f8b2e520a)
2025-10-07 05:27:18 +00:00
ljain112
c42dcbe739 fix: do not fetch disabled item tax template
(cherry picked from commit b10cf4a928)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
#	erpnext/stock/get_item_details.py
2025-10-07 05:26:09 +00:00
rohitwaghchaure
233fe9b7e6 Merge pull request #49929 from frappe/mergify/bp/version-15-hotfix/pr-49928
fix: warning message if the batch has incorrect qty (backport #49928)
2025-10-07 10:42:23 +05:30
Rohit Waghchaure
3f3fd20b31 fix: warning message if the batch has incorrect qty
(cherry picked from commit 870181de87)
2025-10-07 04:50:35 +00:00
ruthra kumar
f84a6c0d3a Merge pull request #49927 from frappe/mergify/bp/version-15-hotfix/pr-49743
fix: use valuation_rate from item master if no bin is present (backport #49743)
2025-10-07 10:03:27 +05:30
ruthra kumar
5c9c17c649 Merge pull request #49925 from frappe/mergify/bp/version-15-hotfix/pr-49870
fix: show asset value as revaluation amount or gross purchase amount (backport #49870)
2025-10-07 10:02:28 +05:30
ravibharathi656
a4b5a74644 fix: use item valuation rate if no bin
(cherry picked from commit 23b1b7ee04)
2025-10-07 04:13:26 +00:00
l0gesh29
ed1c270398 fix: exclude opening entries
(cherry picked from commit 3773f56b0b)
2025-10-06 21:40:58 +00:00
rohitwaghchaure
56d04761f8 Merge pull request #49924 from frappe/mergify/bp/version-15-hotfix/pr-49923
feat: recalculate batch qty (backport #49923)
2025-10-06 22:33:22 +05:30
Rohit Waghchaure
d49b64dc7c feat: recalculate batch qty
(cherry picked from commit 70117d3b06)
2025-10-06 16:30:14 +00:00
Frappe PR Bot
42b43b372a chore(release): Bumped to Version 15.81.3
## [15.81.3](https://github.com/frappe/erpnext/compare/v15.81.2...v15.81.3) (2025-10-06)

### Bug Fixes

* check is_rejected attribute ([b017f07](b017f07343))
* do not consider draft bundles ([92afae7](92afae7185))
2025-10-06 14:45:22 +00:00
rohitwaghchaure
4dae4b987e Merge pull request #49922 from frappe/mergify/bp/version-15/pr-49918
fix: check is_rejected attribute (backport #49913) (backport #49918)
2025-10-06 20:13:46 +05:30
rohitwaghchaure
54e672e078 Merge pull request #49921 from frappe/mergify/bp/version-15/pr-49919
fix: do not consider draft bundles (backport #49917) (backport #49919)
2025-10-06 20:13:38 +05:30
rohitwaghchaure
62a09a3cb0 Merge pull request #49920 from frappe/mergify/bp/version-15-hotfix/pr-49890
perf: serial nos / batches reposting (backport #49890)
2025-10-06 19:58:25 +05:30
Kavin
b017f07343 fix: check is_rejected attribute
(cherry picked from commit 2ac2e02b2f)
(cherry picked from commit bdf150bdf8)
2025-10-06 14:25:55 +00:00
Rohit Waghchaure
92afae7185 fix: do not consider draft bundles
(cherry picked from commit a60f7eaf3a)
(cherry picked from commit 75323fda01)
2025-10-06 14:25:46 +00:00
rohitwaghchaure
eb9d656b8f Merge pull request #49918 from frappe/mergify/bp/version-15-hotfix/pr-49913
fix: check is_rejected attribute (backport #49913)
2025-10-06 19:54:59 +05:30
rohitwaghchaure
7ddf63faa8 Merge pull request #49919 from frappe/mergify/bp/version-15-hotfix/pr-49917
fix: do not consider draft bundles (backport #49917)
2025-10-06 19:54:51 +05:30
Rohit Waghchaure
8a310efc97 perf: serial nos / batches reposting
(cherry picked from commit acb3ef78a7)
2025-10-06 14:11:43 +00:00
Kavin
bdf150bdf8 fix: check is_rejected attribute
(cherry picked from commit 2ac2e02b2f)
2025-10-06 14:08:30 +00:00
Rohit Waghchaure
75323fda01 fix: do not consider draft bundles
(cherry picked from commit a60f7eaf3a)
2025-10-06 14:08:30 +00:00
ruthra kumar
f94f628884 Merge pull request #49912 from frappe/mergify/bp/version-15-hotfix/pr-49600
fix(profit and loss statement): incorrect total calculation (backport #49600)
2025-10-06 17:27:55 +05:30
ravibharathi656
e132c457f2 fix(profit and loss statement): incorrect total calculation
(cherry picked from commit b7c6d8e2a6)
2025-10-06 11:40:38 +00:00
ruthra kumar
048d6f6942 Merge pull request #49908 from frappe/mergify/bp/version-15-hotfix/pr-49635
fix(subscription): include days before (backport #49635)
2025-10-06 15:39:59 +05:30
Raffael Meyer
6c47353205 fix: linter; dont change doc after DB update (#49907)
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2025-10-06 10:02:05 +00:00
ravibharathi656
4f067085e7 test: add invoice generation before period with prorate
(cherry picked from commit b452e06b82)
2025-10-06 09:52:07 +00:00
ravibharathi656
3fcbb10155 refactor(subscription): default prorate 0
(cherry picked from commit eda1dae882)
2025-10-06 09:52:07 +00:00
ravibharathi656
f27b754570 fix(subscription): include days before
(cherry picked from commit 9164162a9e)
2025-10-06 09:52:06 +00:00
mergify[bot]
95387b4bf0 feat: allow fallback to default selling price list (backport #49634) (#49704)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Henning Wendtland <156231187+HenningWendtland@users.noreply.github.com>
2025-10-06 11:49:40 +02:00
ruthra kumar
7f2d26ec20 Merge pull request #49900 from frappe/mergify/bp/version-15-hotfix/pr-49852
fix: SQL operator precedence in Project query customer filter (backport #49852)
2025-10-06 13:21:58 +05:30
Fawaz Alhafiz
d950de2d09 fix: SQL operator precedence in Project query customer filter
Added explicit parentheses around customer OR conditions in get_project_name()
to ensure proper grouping with AND filters. Without these parentheses, SQL
operator precedence caused the status filter to be bypassed when a customer
filter was applied, resulting in completed and cancelled projects appearing
in link field dropdowns.

Before:
WHERE customer='X' OR customer IS NULL OR customer='' AND status NOT IN (...)
was interpreted as:
WHERE customer='X' OR customer IS NULL OR (customer='' AND status NOT IN (...))

After:
WHERE (customer='X' OR customer IS NULL OR customer='') AND status NOT IN (...)

Fixes: Completed/cancelled projects showing in Project link fields
Affected: Any doctype using Project link fields with customer filters
(cherry picked from commit 0ec30a1cea)
2025-10-06 07:34:55 +00:00
ruthra kumar
5d8a368ad2 Merge pull request #49893 from frappe/mergify/bp/version-15-hotfix/pr-49829
fix: Set paid amount automatically only if return entry validated and has negative grand total (backport #49829)
2025-10-06 12:58:33 +05:30
ruthra kumar
615a5fa5ba Merge pull request #49898 from frappe/mergify/bp/version-15-hotfix/pr-49865
fix: delete column dynamically based on the naming by (backport #49865)
2025-10-06 12:51:23 +05:30
ruthra kumar
823cdf211d Merge pull request #49894 from frappe/mergify/bp/version-15-hotfix/pr-49871
fix: retain shipping address in doc (backport #49871)
2025-10-06 12:37:21 +05:30
ravibharathi656
20c21a4dc0 fix: retain shipping address in doc
(cherry picked from commit 039f5e6143)

# Conflicts:
#	erpnext/public/js/controllers/buying.js
2025-10-06 12:35:21 +05:30
l0gesh29
040873a442 fix: delete column dynamically based on the naming by
(cherry picked from commit 4f503ac7f6)
2025-10-06 07:05:04 +00:00
Nabin Hait
a39bc626c7 fix: resolved conflict 2025-10-06 12:17:57 +05:30
Nabin Hait
bd3503a3d8 fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829)
(cherry picked from commit dcbcc596f2)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2025-10-06 05:58:29 +00:00
Frappe PR Bot
161c1fb7a4 chore(release): Bumped to Version 15.81.2
## [15.81.2](https://github.com/frappe/erpnext/compare/v15.81.1...v15.81.2) (2025-10-06)

### Bug Fixes

* failing patch ([65061cc](65061cc459))
2025-10-06 03:54:42 +00:00
ruthra kumar
4d506d2d9c Merge pull request #49873 from frappe/mergify/bp/version-15/pr-49868
fix: failing patch (backport #49867) (backport #49868)
2025-10-06 09:23:17 +05:30
ruthra kumar
04abc6b5e5 Merge pull request #49877 from frappe/mergify/bp/version-15-hotfix/pr-49872
fix: remove allow_on_submit for pick list items (backport #49872)
2025-10-06 09:21:33 +05:30
mergify[bot]
2c0501b05f fix(Common Code): fetch canonical URI from Code List (backport #49882) (#49884)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(Common Code): fetch canonical URI from Code List (#49882)
2025-10-04 19:22:28 +02:00
rohitwaghchaure
7b484de0a9 Merge pull request #49878 from frappe/mergify/bp/version-15-hotfix/pr-49876
fix: optimize SQL query by adding index on batch (backport #49876)
2025-10-04 13:18:07 +05:30
Rohit Waghchaure
a83331bd2f fix: optimize SQL query by adding index on batch
(cherry picked from commit 8756f91857)
2025-10-04 05:30:10 +00:00
Kavin
4ccdedeb12 fix: remove allow_on_submit for pick list items
(cherry picked from commit da716b824f)
2025-10-04 04:36:08 +00:00
Mihir Kandoi
65061cc459 fix: failing patch
(cherry picked from commit 41d1703e7c)
(cherry picked from commit 0dad1957c8)
2025-10-03 19:04:15 +00:00
Mihir Kandoi
fe0739aadf Merge pull request #49868 from frappe/mergify/bp/version-15-hotfix/pr-49867
fix: failing patch (backport #49867)
2025-10-04 00:33:18 +05:30
Mihir Kandoi
0dad1957c8 fix: failing patch
(cherry picked from commit 41d1703e7c)
2025-10-03 10:02:09 +00:00
rohitwaghchaure
bf39ae6db0 Merge pull request #49856 from frappe/mergify/bp/version-15-hotfix/pr-49806
fix: validate transfer_qty based on overproduction wo percentage (backport #49806)
2025-10-02 22:43:52 +05:30
Khushi Rawat
09bcff13ad Merge pull request #49522 from KerollesFathy/clear-custodian-when-asset-take-backed
fix: clear asset custodian when asset take back from employee without assign to another employee
2025-10-02 22:16:35 +05:30
rohitwaghchaure
b91fb6b280 Merge pull request #49855 from frappe/mergify/bp/version-15-hotfix/pr-49850
fix: add default scrap warehouse in wo (backport #49850)
2025-10-02 20:22:20 +05:30
Kavin
0fec34e886 test: test overproduction allowed qty in wo
(cherry picked from commit b527d38bfa)
2025-10-02 14:36:24 +00:00
Kavin
b6d57ff8a5 fix: set fg_completed_qty based upon fg item qty
(cherry picked from commit 526b850e61)
2025-10-02 14:36:24 +00:00
Kavin
7ce97ce0c2 fix: validate transfer_qty based on overproduction wo percentage
(cherry picked from commit 4024d8846b)
2025-10-02 14:36:23 +00:00
Kavin
a5ed9fdc67 fix: add default scrap warehouse in wo
(cherry picked from commit 7e51346946)
2025-10-02 14:35:22 +00:00
mergify[bot]
5e5850d89a refactor(Supplier): custom buttons call make methods (backport #49840) (#49842)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-10-02 00:16:34 +02:00
Diptanil Saha
56aa86f77e Merge pull request #49839 from diptanilsaha/backport_49496
fix(Accounts Payable Summary): add a missing translate function (backport #49496)
2025-10-02 00:24:42 +05:30
Diptanil Saha
f321725b49 Merge pull request #49496 from elshafei-developer/Add-a-missing-translate-function
fix(Accounts Payable Summary): add a missing translate function
2025-10-02 00:07:06 +05:30
Diptanil Saha
4e3697284e Merge pull request #49838 from diptanilsaha/backport_49820
fix: financial ratios translation and pdf export error (backport #49820)
2025-10-01 23:57:09 +05:30
Frappe PR Bot
4c1cada222 chore(release): Bumped to Version 15.81.1
## [15.81.1](https://github.com/frappe/erpnext/compare/v15.81.0...v15.81.1) (2025-10-01)

### Bug Fixes

* Add try-finally for setting buying price list ([e10ed89](e10ed89be5))
* too many writes on patch run ([c455e73](c455e7390e))
2025-10-01 14:55:26 +00:00
Mihir Kandoi
8bb19eece4 Merge pull request #49835 from frappe/mergify/bp/version-15/pr-49833
fix: too many writes on patch run (backport #49832) (backport #49833)
2025-10-01 20:23:45 +05:30
Mihir Kandoi
e10ed89be5 fix: Add try-finally for setting buying price list
(cherry picked from commit 35a8d02866)
(cherry picked from commit e21baec246)
2025-10-01 13:44:41 +00:00
Mihir Kandoi
c455e7390e fix: too many writes on patch run
(cherry picked from commit 44ff6ed6a1)
(cherry picked from commit a2bf53ff0a)
2025-10-01 13:44:41 +00:00
Mihir Kandoi
3f9fbb7b75 Merge pull request #49833 from frappe/mergify/bp/version-15-hotfix/pr-49832 2025-10-01 19:13:21 +05:30
Mihir Kandoi
e21baec246 fix: Add try-finally for setting buying price list
(cherry picked from commit 35a8d02866)
2025-10-01 13:27:40 +00:00
Mihir Kandoi
a2bf53ff0a fix: too many writes on patch run
(cherry picked from commit 44ff6ed6a1)
2025-10-01 13:27:40 +00:00
ruthra kumar
06cd48feaa Merge pull request #49823 from frappe/mergify/bp/version-15-hotfix/pr-49689
fix(accounting): ensure proper removal of advance references during u… (backport #49689)
2025-10-01 12:56:19 +05:30
Pandiyan P
825ccd3422 fix(accounting): ensure proper removal of advance references during unreconcillation
(cherry picked from commit a7ec01bf21)
2025-10-01 06:40:55 +00:00
rohitwaghchaure
c56da286f7 Merge pull request #49811 from frappe/mergify/bp/version-15-hotfix/pr-49750
fix: get unconsumed qty as per BOM required qty (backport #49750)
2025-09-30 22:19:55 +05:30
Khushi Rawat
a7577d428f Merge pull request #49817 from khushi8112/backport-v15-pr-43093
feat: validating asset scrap date (backport #43093)
2025-09-30 21:23:26 +05:30
Khushi Rawat
e81b85b241 chore: resolve conflict 2025-09-30 21:02:39 +05:30
rahulgupta8848
0eb76f4d2c feat: validating asset scrap date (#43093)
* feat: validating asset scrap date

* refactor: refactorization of scrap asset function

---------

Co-authored-by: “rahulgupta8848” <“rahul.gupta@8848digital.com”>
(cherry picked from commit e07bc5af41)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.js
2025-09-30 21:02:15 +05:30
Frappe PR Bot
24013ddf27 chore(release): Bumped to Version 15.81.0
# [15.81.0](https://github.com/frappe/erpnext/compare/v15.80.1...v15.81.0) (2025-09-30)

### Bug Fixes

* add date filter for getting return invoice items ([bc77704](bc77704462))
* auto batch not set for raw materials in subcontracting receipt ([6c8e838](6c8e8384d5))
* **bank transaction:** change reference number to small text ([5a0bf7a](5a0bf7a5e1))
* convert with flt ([3975627](3975627b33))
* do not validate cost center in cancelled gl entry ([366bb82](366bb8269c))
* don't recalculate stock_qty with conversion_factor ([c88a277](c88a277ad9))
* include precision in validation ([abffbca](abffbcad99))
* **payment entry:** trigger currency on account set ([84a2596](84a2596ce5))
* remove item name to avoid overriding item row name ([d0e511f](d0e511fd2f))
* set cost center in taxes if not set ([4dcd502](4dcd502b76))
* **stock entry:** set expense account from company for manufacture ([50f9521](50f9521feb))
* update item details only in draft state ([30f6b53](30f6b53240))
* update subcontracted_quantity with set_value ([17e415d](17e415d03a))
* use get_value instead of get_doc ([f706811](f706811757))
* use sales_order from data instead of doc ([7ee8281](7ee82813fd))
* use stock adjustment account  if no expense account ([9c81f44](9c81f448c8))
* valuation rate for old batch ([37a03f1](37a03f10ab))

### Features

* add show zero value filter in profit and loss and balance sheet ([c32ad97](c32ad9711f))
* service expense account in the subcontracting receipt ([ba9b63a](ba9b63af49))

### Performance Improvements

* reposting for backdated transactions ([cd5a836](cd5a836685))
2025-09-30 13:28:59 +00:00
ruthra kumar
e8ee5f8c9b Merge pull request #49801 from frappe/version-15-hotfix
chore: release v15
2025-09-30 18:56:45 +05:30
rohitwaghchaure
0fb5f75e93 chore: fix conflicts
Removed the test for additional material transfer in work orders.
2025-09-30 18:43:38 +05:30
rohitwaghchaure
4847e24553 Merge branch 'version-15' into version-15-hotfix 2025-09-30 18:38:20 +05:30
Kavin
f548f0b231 test: required_qty clamping in manufacture entry
(cherry picked from commit 34d2c8d9c2)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2025-09-30 12:28:34 +00:00
Kavin
82386b18aa fix: get unconsumed qty as per BOM qty
(cherry picked from commit cf4b395ee3)
2025-09-30 12:28:33 +00:00
rohitwaghchaure
bbbcb456ce Merge pull request #49810 from frappe/mergify/bp/version-15-hotfix/pr-49803
fix: valuation rate for old batch (backport #49803)
2025-09-30 17:50:13 +05:30
rohitwaghchaure
4c78f9b9cb Merge pull request #49807 from frappe/mergify/bp/version-15-hotfix/pr-49794
fix: don't recalculate stock_qty with conversion_factor (backport #49794)
2025-09-30 17:12:18 +05:30
Rohit Waghchaure
37a03f10ab fix: valuation rate for old batch
(cherry picked from commit d864d166f9)
2025-09-30 11:42:15 +00:00
rohitwaghchaure
c2dd37a89e Merge pull request #49808 from frappe/mergify/bp/version-15-hotfix/pr-49766
fix: use sales_order from data instead of doc (backport #49766)
2025-09-30 17:12:07 +05:30
rohitwaghchaure
f458faa819 Merge pull request #49809 from frappe/mergify/bp/version-15-hotfix/pr-49748
fix: update item details only in draft state (backport #49748)
2025-09-30 17:11:56 +05:30
Kavin
30f6b53240 fix: update item details only in draft state
(cherry picked from commit 689172ff22)
2025-09-30 11:08:59 +00:00
Kavin
7ee82813fd fix: use sales_order from data instead of doc
(cherry picked from commit 9f9120451b)
2025-09-30 11:06:48 +00:00
Kavin
c88a277ad9 fix: don't recalculate stock_qty with conversion_factor
(cherry picked from commit fed8236919)
2025-09-30 11:03:47 +00:00
rohitwaghchaure
9e73d083b5 Merge pull request #49793 from frappe/mergify/bp/version-15-hotfix/pr-49790
fix: update subcontracted_quantity with set_value (backport #49790)
2025-09-30 16:32:13 +05:30
rohitwaghchaure
03c8d92796 Merge pull request #49792 from frappe/mergify/bp/version-15-hotfix/pr-49791
refactor: convert item warehouse based reposting (backport #49791)
2025-09-30 16:31:57 +05:30
ruthra kumar
31ad9a6346 Merge pull request #49805 from frappe/mergify/bp/version-15-hotfix/pr-49639
fix(Credit-limit): consider current voucher for credit limit validation (backport #49639)
2025-09-30 16:30:17 +05:30
PRASATHRAJA
fc40a3c376 Merge pull request #49639 from aerele/credit-limit-jv
fix(Credit-limit): consider current voucher for credit limit validation

(cherry picked from commit 4a01c53cca)
2025-09-30 10:39:35 +00:00
ruthra kumar
b8e3db0179 Merge pull request #49800 from frappe/mergify/bp/version-15-hotfix/pr-49799
fix: do not validate cost center in cancelled gl entry (backport #49799)
2025-09-30 15:14:11 +05:30
ljain112
366bb8269c fix: do not validate cost center in cancelled gl entry
(cherry picked from commit 29cbddbc77)
2025-09-30 09:04:24 +00:00
venkat102
3975627b33 fix: convert with flt
(cherry picked from commit 81614939ab)
2025-09-29 16:20:27 +00:00
venkat102
f706811757 fix: use get_value instead of get_doc
(cherry picked from commit 89a603f20c)
2025-09-29 16:20:26 +00:00
venkat102
17e415d03a fix: update subcontracted_quantity with set_value
(cherry picked from commit ea63bfc9af)
2025-09-29 16:20:26 +00:00
Rohit Waghchaure
8911bad792 refactor: convert item-wh based reposting
(cherry picked from commit 8411e4c5b2)
2025-09-29 16:20:25 +00:00
ruthra kumar
a30c481ca1 Merge pull request #49787 from frappe/mergify/bp/version-15-hotfix/pr-49773
fix(bank transaction): change reference number to small text (backport #49773)
2025-09-29 17:15:06 +05:30
ravibharathi656
5a0bf7a5e1 fix(bank transaction): change reference number to small text
(cherry picked from commit 90f399d0fc)

# Conflicts:
#	erpnext/accounts/doctype/bank_transaction/bank_transaction.json
2025-09-29 16:59:31 +05:30
ruthra kumar
f135954944 Merge pull request #49784 from frappe/mergify/bp/version-15-hotfix/pr-49708
feat: add show zero value filter in profit and loss and balance sheet (backport #49708)
2025-09-29 16:47:54 +05:30
ravibharathi656
c32ad9711f feat: add show zero value filter in profit and loss and balance sheet
(cherry picked from commit 33ab24943c)

# Conflicts:
#	erpnext/accounts/report/balance_sheet/balance_sheet.js
#	erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
2025-09-29 16:32:21 +05:30
ruthra kumar
f6842041c9 Merge pull request #49782 from frappe/mergify/bp/version-15-hotfix/pr-49718
fix: set cost center in taxes if not set (backport #49718)
2025-09-29 16:26:35 +05:30
ravibharathi656
4dcd502b76 fix: set cost center in taxes if not set
(cherry picked from commit b75940bf0e)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2025-09-29 15:53:19 +05:30
ruthra kumar
19f81c9204 Merge pull request #49777 from frappe/mergify/bp/version-15-hotfix/pr-49735
fix(payment entry): trigger currency on account set (backport #49735)
2025-09-29 14:56:14 +05:30
ruthra kumar
67b6b3779d Merge pull request #49775 from frappe/mergify/bp/version-15-hotfix/pr-49618
fix: add date filter for getting return invoice items (backport #49618)
2025-09-29 14:31:45 +05:30
ruthra kumar
c836016358 Merge pull request #49774 from frappe/mergify/bp/version-15-hotfix/pr-49640
fix: include precision in validation (backport #49640)
2025-09-29 14:29:45 +05:30
ravibharathi656
84a2596ce5 fix(payment entry): trigger currency on account set
(cherry picked from commit 096e74b1ee)
2025-09-29 08:58:03 +00:00
Navin-S-R
98bc7b41fc test: validate profit values for later period returns
(cherry picked from commit d5c457b8c5)

# Conflicts:
#	erpnext/accounts/report/gross_profit/test_gross_profit.py
2025-09-29 14:16:52 +05:30
l0gesh29
abffbcad99 fix: include precision in validation
(cherry picked from commit 1de0c46c51)
2025-09-29 08:44:29 +00:00
Navin-S-R
bc77704462 fix: add date filter for getting return invoice items
(cherry picked from commit 2abb011816)
2025-09-29 08:44:28 +00:00
rohitwaghchaure
5abb2d510d Merge pull request #49772 from frappe/mergify/bp/version-15-hotfix/pr-49770
chore: removed print statement (backport #49770)
2025-09-29 13:06:44 +05:30
Rohit Waghchaure
ae040024af chore: removed print statement
(cherry picked from commit 324bdcb177)
2025-09-29 06:32:32 +00:00
rohitwaghchaure
fe1247dd4b Merge pull request #49758 from frappe/mergify/bp/version-15-hotfix/pr-49757
feat: service expense account in the subcontracting receipt (backport #49757)
2025-09-27 10:20:23 +05:30
rohitwaghchaure
84789daf59 Merge pull request #49756 from frappe/mergify/bp/version-15-hotfix/pr-49741
fix(stock entry): set expense account from company for manufacture (backport #49741)
2025-09-27 10:04:06 +05:30
rohitwaghchaure
cecd177286 chore: fix conflicts
Removed fields related to job card and landed cost voucher amount from subcontracting receipt item.
2025-09-27 10:03:27 +05:30
rohitwaghchaure
c26ed80d67 chore: fix conflicts 2025-09-27 10:02:40 +05:30
rohitwaghchaure
15f0c28cda chore: fix conflicts 2025-09-27 10:01:53 +05:30
Rohit Waghchaure
ba9b63af49 feat: service expense account in the subcontracting receipt
(cherry picked from commit 6e597b9c42)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
#	erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
2025-09-26 15:44:26 +00:00
ravibharathi656
9c81f448c8 fix: use stock adjustment account if no expense account
(cherry picked from commit b2e109318f)
2025-09-26 13:28:43 +00:00
ravibharathi656
50f9521feb fix(stock entry): set expense account from company for manufacture
(cherry picked from commit 06177ffaff)
2025-09-26 13:28:43 +00:00
ruthra kumar
0ff32ac554 Merge pull request #49738 from frappe/mergify/bp/version-15-hotfix/pr-49717
refactor: improve trial balance performance (backport #49717)
2025-09-26 11:58:26 +05:30
rohitwaghchaure
fe72ea6d6b Merge pull request #49739 from frappe/mergify/bp/version-15-hotfix/pr-49734
fix: auto batch not set for raw materials in subcontracting receipt (backport #49734)
2025-09-26 11:43:04 +05:30
rohitwaghchaure
62959e5a70 chore: fix conflicts 2025-09-26 11:21:16 +05:30
Rohit Waghchaure
6c8e8384d5 fix: auto batch not set for raw materials in subcontracting receipt
(cherry picked from commit 23f9d4c600)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py
2025-09-26 05:07:12 +00:00
ruthra kumar
0c8004530a refactor: improve trial balance performance
(cherry picked from commit cee3813ced)
2025-09-26 04:46:00 +00:00
Raffael Meyer
b18afa8bdd chore(Bank Guarante): move to leaner doctype definition (#49737) 2025-09-25 22:05:59 +00:00
Frappe PR Bot
f54933e657 chore(release): Bumped to Version 15.80.1
## [15.80.1](https://github.com/frappe/erpnext/compare/v15.80.0...v15.80.1) (2025-09-25)

### Bug Fixes

* remove item name to avoid overriding item row name ([a80e0d9](a80e0d9d39))

### Performance Improvements

* reposting for backdated transactions ([f43fac5](f43fac50ed))
2025-09-25 15:14:17 +00:00
rohitwaghchaure
a1862020e8 Merge pull request #49732 from frappe/mergify/bp/version-15/pr-49728
fix: remove item name in get_item_details (backport #49725) (backport #49728)
2025-09-25 20:42:44 +05:30
rohitwaghchaure
249d6b3619 Merge pull request #49729 from frappe/mergify/bp/version-15/pr-49724
perf: reposting for backdated transactions (backport #49720) (backport #49724)
2025-09-25 20:42:35 +05:30
Kavin
a80e0d9d39 fix: remove item name to avoid overriding item row name
(cherry picked from commit 47055901c0)
(cherry picked from commit d0e511fd2f)
2025-09-25 14:23:33 +00:00
rohitwaghchaure
6b3bb45900 Merge pull request #49728 from frappe/mergify/bp/version-15-hotfix/pr-49725
fix: remove item name in get_item_details (backport #49725)
2025-09-25 19:52:28 +05:30
Rohit Waghchaure
f43fac50ed perf: reposting for backdated transactions
(cherry picked from commit 1b0fc0541b)
(cherry picked from commit cd5a836685)
2025-09-25 13:23:30 +00:00
Kavin
d0e511fd2f fix: remove item name to avoid overriding item row name
(cherry picked from commit 47055901c0)
2025-09-25 13:23:29 +00:00
rohitwaghchaure
d50538dbf7 Merge pull request #49724 from frappe/mergify/bp/version-15-hotfix/pr-49720
perf: reposting for backdated transactions (backport #49720)
2025-09-25 18:52:32 +05:30
Rohit Waghchaure
cd5a836685 perf: reposting for backdated transactions
(cherry picked from commit 1b0fc0541b)
2025-09-25 12:16:41 +00:00
KerollesFathy
4bc76be130 refactor: optimize asset location and custodian update logic to avoid multiple updates 2025-09-10 19:01:14 +03:00
KerollesFathy
f4cdb49126 fix: clear asset custodian when asset take back from employee without assign to another employee 2025-09-10 18:27:39 +03:00
86 changed files with 1880 additions and 1430 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,7 @@
{
"allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Reference Number"
},
{
@@ -238,7 +238,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-06-18 17:24:57.044666",
"modified": "2025-09-26 17:06:29.207673",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -36,7 +36,7 @@ class BankTransaction(Document):
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.Data | None
reference_number: DF.SmallText | None
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
transaction_id: DF.Data | None
transaction_type: DF.Data | None

View File

@@ -253,7 +253,7 @@ class GLEntry(Document):
)
def validate_cost_center(self):
if not self.cost_center:
if not self.cost_center or self.is_cancelled:
return
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])

View File

@@ -189,8 +189,8 @@ class JournalEntry(AccountsController):
def on_submit(self):
self.validate_cheque_info()
self.check_credit_limit()
self.make_gl_entries()
self.check_credit_limit()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()

View File

@@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
class TestJournalEntry(unittest.TestCase):
@@ -592,6 +593,15 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def make_journal_entry(
account1,

View File

@@ -601,6 +601,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm);
}
frm.events.paid_from_account_currency(frm);
}
);
},
@@ -624,6 +625,7 @@ frappe.ui.form.on("Payment Entry", {
frm.events.received_amount(frm);
}
}
frm.events.paid_to_account_currency(frm);
}
);
},

View File

@@ -127,7 +127,13 @@ class PaymentRequest(Document):
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
if (
flt(
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
get_currency_precision(),
)
> ref_amount
):
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype

View File

@@ -10,14 +10,19 @@
"description",
"section_break_4",
"due_date",
"invoice_portion",
"mode_of_payment",
"column_break_5",
"invoice_portion",
"due_date_based_on",
"credit_days",
"credit_months",
"section_break_6",
"discount_type",
"discount_date",
"column_break_9",
"discount",
"discount_type",
"column_break_9",
"discount_validity_based_on",
"discount_validity",
"section_break_9",
"payment_amount",
"outstanding",
@@ -172,12 +177,50 @@
"label": "Paid Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"label": "Credit Days",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "discount",
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "discount_validity_based_on",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-11 11:06:51.792982",
"modified": "2025-07-31 08:38:25.820701",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -189,4 +232,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,12 +17,27 @@ class PaymentSchedule(Document):
base_outstanding: DF.Currency
base_paid_amount: DF.Currency
base_payment_amount: DF.Currency
credit_days: DF.Int
credit_months: DF.Int
description: DF.SmallText | None
discount: DF.Float
discount_date: DF.Date | None
discount_type: DF.Literal["Percentage", "Amount"]
discount_validity: DF.Int
discount_validity_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
discounted_amount: DF.Currency
due_date: DF.Date
due_date_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
invoice_portion: DF.Percent
mode_of_payment: DF.Link | None
outstanding: DF.Currency

View File

@@ -161,4 +161,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -2150,19 +2150,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(rate, 500)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
@@ -2188,7 +2185,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",

View File

@@ -483,18 +483,23 @@ class Subscription(Document):
return invoice
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]:
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
"""
Returns the `Item`s linked to `Subscription Plan`
"""
if prorate is None:
prorate = False
prorate_factor = 1
if prorate:
prorate_factor = get_prorata_factor(
self.current_invoice_end,
self.current_invoice_start,
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
cint(
self.generate_invoice_at
in [
"Beginning of the current subscription period",
"Days before the current subscription period",
]
),
)
items = []
@@ -511,33 +516,19 @@ class Subscription(Document):
deferred = frappe.db.get_value("Item", item_code, deferred_field)
if not prorate:
item = {
"item_code": item_code,
"qty": plan.qty,
"rate": get_plan_rate(
plan.plan,
plan.qty,
party,
self.current_invoice_start,
self.current_invoice_end,
),
"cost_center": plan_doc.cost_center,
}
else:
item = {
"item_code": item_code,
"qty": plan.qty,
"rate": get_plan_rate(
plan.plan,
plan.qty,
party,
self.current_invoice_start,
self.current_invoice_end,
prorate_factor,
),
"cost_center": plan_doc.cost_center,
}
item = {
"item_code": item_code,
"qty": plan.qty,
"rate": get_plan_rate(
plan.plan,
plan.qty,
party,
self.current_invoice_start,
self.current_invoice_end,
prorate_factor,
),
"cost_center": plan_doc.cost_center,
}
if deferred:
item.update(

View File

@@ -8,6 +8,7 @@ from frappe.utils.data import (
add_days,
add_months,
add_to_date,
add_years,
cint,
date_diff,
flt,
@@ -555,6 +556,33 @@ class TestSubscription(FrappeTestCase):
subscription.reload()
self.assertEqual(len(subscription.invoices), 0)
def test_invoice_generation_days_before_subscription_period_with_prorate(self):
settings = frappe.get_single("Subscription Settings")
settings.prorate = 1
settings.save()
create_plan(
plan_name="_Test Plan Name 5",
cost=1000,
billing_interval="Year",
billing_interval_count=1,
currency="INR",
)
start_date = add_days(nowdate(), 2)
subscription = create_subscription(
start_date=start_date,
party_type="Supplier",
party="_Test Supplier",
generate_invoice_at="Days before the current subscription period",
generate_new_invoices_past_due_date=1,
number_of_days=2,
plans=[{"plan": "_Test Plan Name 5", "qty": 1}],
)
subscription.process(nowdate())
self.assertEqual(len(subscription.invoices), 1)
def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")

View File

@@ -1270,7 +1270,7 @@ class ReceivablePayableReport:
def setup_ageing_columns(self):
# for charts
self.ageing_column_labels = []
ranges = [*self.ranges, "Above"]
ranges = [*self.ranges, _("Above")]
prev_range_value = 0
for idx, curr_range_value in enumerate(ranges):

View File

@@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_("Difference"), fieldname="diff")
self.setup_ageing_columns()
self.add_column(label="Total Amount Due", fieldname="total_due")
self.add_column(label=_("Total Amount Due"), fieldname="total_due")
if self.filters.show_future_payments:
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")

View File

@@ -5,28 +5,33 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
erpnext.utils.add_dimensions("Balance Sheet", 10);
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
],
default: "Report",
reqd: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push(
{
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
],
default: "Report",
reqd: 1,
},
{
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
}
);

View File

@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
},
],
formatter: function (value, row, column, data, default_formatter) {
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
if (heading_ratios.includes(value)) {
value = $(`<span>${value}</span>`);
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
value = $value.wrap("<p></p>").parent().html();
}
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
column.fieldtype = "Data";
}

View File

@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Liquidity Ratios"})
data.append({"ratio": _("Liquidity Ratios")})
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
for d in ratio_data:
row = {
@@ -165,13 +165,13 @@ def add_solvency_ratios(
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Solvency Ratios"})
data.append({"ratio": _("Solvency Ratios")})
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
net_profit_ratio = {"ratio": "Net Profit Ratio"}
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
@@ -195,7 +195,7 @@ def add_solvency_ratios(
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Turnover Ratios"})
data.append({"ratio": _("Turnover Ratios")})
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
)
ratio_data = [
["Fixed Asset Turnover Ratio", net_sales, total_asset],
["Debtor Turnover Ratio", net_sales, avg_debtors],
["Creditor Turnover Ratio", direct_expense, avg_creditors],
["Inventory Turnover Ratio", cogs, avg_stock],
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
[_("Inventory Turnover Ratio"), cogs, avg_stock],
]
for ratio in ratio_data:
row = {

View File

@@ -212,7 +212,7 @@ def get_data(
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map)
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
if out and total:
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
@@ -323,18 +323,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
def get_all_parents(account, parent_children_map):
for parent, children in parent_children_map.items():
for child in children:
if child["name"] == account and parent:
accounts_to_show.add(parent)
get_all_parents(parent, parent_children_map)
data_with_value = []
accounts_to_show = set()
for d in data:
if show_zero_values or d.get("has_value"):
accounts_to_show.add(d.get("account"))
get_all_parents(d.get("account"), parent_children_map)
for d in data:
if d.get("account") in accounts_to_show:
data_with_value.append(d)
else:
# show group with zero balance, if there are balances against child
children = [child.name for child in parent_children_map.get(d.get("account")) or []]
if children:
for row in data:
if row.get("account") in children and row.get("has_value"):
data_with_value.append(d)
break
return data_with_value

View File

@@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
# to display item as Item Code: Item Name
columns[0] = "Sales Invoice:Link/Item:300"
# removing Item Code and Item Name columns
del columns[4:6]
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name":
del columns[4:6]
else:
del columns[5:7]
total_base_amount = 0
total_buying_amount = 0
@@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters):
"label": _("Posting Date"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 100,
"width": 120,
},
"posting_time": {
"label": _("Posting Time"),
@@ -677,7 +682,9 @@ class GrossProfitGenerator:
si.name = si_item.parent
and si.docstatus = 1
and si.is_return = 1
and si.posting_date between %(from_date)s and %(to_date)s
""",
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
as_dict=1,
)

View File

@@ -1,9 +1,9 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
@@ -392,7 +392,6 @@ class TestGrossProfit(FrappeTestCase):
"""
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Invoice with an item added twice
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
@@ -635,3 +634,42 @@ class TestGrossProfit(FrappeTestCase):
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
# create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = month_start_date
sinv.save().submit()
# create credit note on next month start date
cr_note = make_sales_return(sinv.name)
cr_note.set_posting_time = 1
cr_note.posting_date = add_days(month_end_date, 1)
cr_note.save().submit()
# apply filters for invoiced period
filters = frappe._dict(
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
)
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
# extend filters upto returned period
filters.update(to_date=add_days(month_end_date, 1))
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 0.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 0.0)
self.assertEqual(total.get("gross_profit_%"), 0.0)

View File

@@ -5,29 +5,34 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
{ value: "Margin", label: __("Margin View") },
],
default: "Report",
reqd: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
{ value: "Margin", label: __("Margin View") },
],
default: "Report",
reqd: 1,
},
{
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
}
);

View File

@@ -216,7 +216,7 @@ def get_opening_balance(
ignore_is_opening=0,
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
opening_balance = (
frappe.qb.from_(closing_balance)
@@ -228,14 +228,7 @@ def get_opening_balance(
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"),
)
.where(
(closing_balance.company == filters.company)
& (
closing_balance.account.isin(
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
)
)
)
.where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts)))
.groupby(closing_balance.account)
)
@@ -290,21 +283,24 @@ def get_opening_balance(
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
)
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
if accounting_dimensions:
for dimension in accounting_dimensions:

View File

@@ -947,19 +947,28 @@ def update_accounting_ledgers_after_reference_removal(
adv_ple.run()
def remove_ref_from_advance_section(ref_doc: object = None):
def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None):
# TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
ref_doc.set("advances", [])
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
row_names = []
for adv in ref_doc.get("advances") or []:
if adv.get("reference_name", None) == payment_name:
row_names.append(adv.name)
if not row_names:
return
child_table = (
"Sales Invoice Advance" if ref_doc.doctype == "Sales Invoice" else "Purchase Invoice Advance"
)
frappe.db.delete(child_table, {"name": ("in", row_names)})
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_from_advance_section(ref_doc)
remove_ref_from_advance_section(ref_doc, payment_name)
def remove_ref_doc_link_from_jv(
@@ -1026,7 +1035,6 @@ def remove_ref_doc_link_from_pe(
query = query.where(per.parent == payment_name)
reference_rows = query.run(as_dict=True)
if not reference_rows:
return

View File

@@ -790,17 +790,33 @@ frappe.ui.form.on("Asset Finance Book", {
});
erpnext.asset.scrap_asset = function (frm) {
frappe.confirm(__("Do you really want to scrap this asset?"), function () {
frappe.call({
args: {
asset_name: frm.doc.name,
var scrap_dialog = new frappe.ui.Dialog({
title: __("Enter date to scrap asset"),
fields: [
{
label: __("Select the date"),
fieldname: "scrap_date",
fieldtype: "Date",
reqd: 1,
},
method: "erpnext.assets.doctype.asset.depreciation.scrap_asset",
callback: function (r) {
cur_frm.reload_doc();
},
});
],
size: "medium",
primary_action_label: "Submit",
primary_action(values) {
frappe.call({
args: {
asset_name: frm.doc.name,
scrap_date: values.scrap_date,
},
method: "erpnext.assets.doctype.asset.depreciation.scrap_asset",
callback: function (r) {
frm.reload_doc();
scrap_dialog.hide();
},
});
},
});
scrap_dialog.show();
};
erpnext.asset.restore_asset = function (frm) {

View File

@@ -394,7 +394,7 @@ def get_comma_separated_links(names, doctype):
@frappe.whitelist()
def scrap_asset(asset_name):
def scrap_asset(asset_name, scrap_date=None):
asset = frappe.get_doc("Asset", asset_name)
if asset.docstatus != 1:
@@ -402,7 +402,11 @@ def scrap_asset(asset_name):
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
date = today()
today_date = getdate(today())
date = getdate(scrap_date) or today_date
purchase_date = getdate(asset.purchase_date)
validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name)
notes = _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
@@ -436,6 +440,36 @@ def scrap_asset(asset_name):
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name):
if scrap_date > today_date:
frappe.throw(_("Future date is not allowed"))
elif scrap_date < purchase_date:
frappe.throw(_("Scrap date cannot be before purchase date"))
if calculate_depreciation:
asset_depreciation_schedules = frappe.db.get_all(
"Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"]
)
for depreciation_schedule in asset_depreciation_schedules:
last_booked_depreciation_date = frappe.db.get_value(
"Depreciation Schedule",
{
"parent": depreciation_schedule["name"],
"docstatus": 1,
"journal_entry": ["!=", ""],
},
"schedule_date",
order_by="schedule_date desc",
)
if (
last_booked_depreciation_date
and scrap_date < last_booked_depreciation_date
and scrap_date > purchase_date
):
frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
@frappe.whitelist()
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)

View File

@@ -15,6 +15,7 @@ from frappe.utils import (
is_last_day_of_the_month,
nowdate,
)
from frappe.utils.data import add_to_date
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -219,6 +220,31 @@ class TestAsset(AssetSetup):
)
self.assertEqual(accumulated_depr_amount, 18000.0)
asset_depreciation = frappe.db.get_value(
"Asset Depreciation Schedule", {"asset": asset.name, "docstatus": 1}, "name"
)
last_booked_depreciation_date = frappe.db.get_value(
"Depreciation Schedule",
{
"parent": asset_depreciation,
"docstatus": 1,
"journal_entry": ["!=", ""],
},
"schedule_date",
order_by="schedule_date desc",
)
before_purchase_date = add_to_date(asset.purchase_date, days=-1)
future_date = add_to_date(nowdate(), days=1)
if last_booked_depreciation_date:
before_last_booked_depreciation_date = add_to_date(last_booked_depreciation_date, days=-1)
self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_purchase_date)
self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=future_date)
self.assertRaises(
frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date
)
scrap_asset(asset.name)
asset.load_from_db()
first_asset_depr_schedule.load_from_db()

View File

@@ -142,10 +142,18 @@ class AssetMovement(Document):
def update_asset_location_and_custodian(self, asset_id, location, employee):
asset = frappe.get_doc("Asset", asset_id)
updates = {}
if employee and employee != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", employee)
updates["custodian"] = employee
elif not employee and asset.custodian:
updates["custodian"] = ""
if location and location != asset.location:
frappe.db.set_value("Asset", asset_id, "location", location)
updates["location"] = location
if updates:
frappe.db.set_value("Asset", asset_id, updates)
def log_asset_activity(self, asset_id, location, employee):
if location and employee:

View File

@@ -268,6 +268,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
.where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account))
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(gle.is_opening == "No")
.where(company.name == filters.company)
.where(asset.docstatus == 1)
)

View File

@@ -541,12 +541,8 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertRaises(frappe.ValidationError, pr.submit)
self.assertRaises(frappe.ValidationError, pi.submit)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_make_purchase_invoice_with_terms(self):
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
automatically_fetch_payment_terms()
po = create_purchase_order(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
@@ -570,7 +566,6 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
automatically_fetch_payment_terms(enable=0)
def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany
@@ -718,6 +713,7 @@ class TestPurchaseOrder(FrappeTestCase):
)
self.assertEqual(due_date, "2023-03-31")
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
@@ -905,18 +901,16 @@ class TestPurchaseOrder(FrappeTestCase):
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
compare_payment_schedules,
)
automatically_fetch_payment_terms()
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
create_payment_terms_template()
po.payment_terms_template = "Test Receivable Template"
@@ -930,8 +924,6 @@ class TestPurchaseOrder(FrappeTestCase):
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi)
automatically_fetch_payment_terms(enable=0)
def test_internal_transfer_flow(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (

View File

@@ -109,21 +109,9 @@ frappe.ui.form.on("Supplier", {
__("View")
);
frm.add_custom_button(
__("Bank Account"),
function () {
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
},
__("Create")
);
frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create"));
frm.add_custom_button(
__("Pricing Rule"),
function () {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
},
__("Create")
);
frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create"));
frm.add_custom_button(
__("Get Supplier Group Details"),

View File

@@ -231,6 +231,11 @@ class AccountsController(TransactionBase):
self.validate_date_with_fiscal_year()
self.validate_party_accounts()
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
if self.is_return:
self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
self.validate_inter_company_reference()
# validate inter company transaction rate
@@ -282,11 +287,6 @@ class AccountsController(TransactionBase):
self.set_advance_gain_or_loss()
if self.is_return:
self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
@@ -2558,6 +2558,7 @@ class AccountsController(TransactionBase):
self.payment_schedule = []
self.payment_terms_template = po_or_so.payment_terms_template
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
for schedule in po_or_so.payment_schedule:
payment_schedule = {
@@ -2570,6 +2571,17 @@ class AccountsController(TransactionBase):
}
if automatically_fetch_payment_terms:
if schedule.due_date_based_on:
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
payment_schedule["credit_days"] = cint(schedule.credit_days)
payment_schedule["credit_months"] = cint(schedule.credit_months)
if schedule.discount_validity_based_on:
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
payment_schedule["payment_amount"] = flt(
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
schedule.precision("payment_amount"),
@@ -3369,14 +3381,27 @@ def get_payment_term_details(
term = frappe.get_doc("Payment Term", term)
else:
term_details.payment_term = term.payment_term
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
fields_to_copy = [
"description",
"invoice_portion",
"discount_type",
"discount",
"mode_of_payment",
"due_date_based_on",
"credit_days",
"credit_months",
"discount_validity_based_on",
"discount_validity",
]
for field in fields_to_copy:
term_details[field] = term.get(field)
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
term_details.discount_type = term.discount_type
term_details.discount = term.discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment
term_details.base_outstanding = term_details.base_payment_amount
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
@@ -3395,11 +3420,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
date = bill_date or posting_date
if term.due_date_based_on == "Day(s) after invoice date":
due_date = add_days(date, term.credit_days)
due_date = add_days(date, cint(term.credit_days))
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
due_date = add_days(get_last_day(date), term.credit_days)
due_date = add_days(get_last_day(date), cint(term.credit_days))
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
due_date = get_last_day(add_months(date, term.credit_months))
due_date = get_last_day(add_months(date, cint(term.credit_months)))
return due_date
@@ -3407,11 +3432,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
discount_validity = None
date = bill_date or posting_date
if term.discount_validity_based_on == "Day(s) after invoice date":
discount_validity = add_days(date, term.discount_validity)
discount_validity = add_days(date, cint(term.discount_validity))
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
discount_validity = add_days(get_last_day(date), term.discount_validity)
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
discount_validity = get_last_day(add_months(date, term.discount_validity))
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
return discount_validity

View File

@@ -313,7 +313,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
if filters:
if filters.get("customer"):
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
)
if filters.get("company"):

View File

@@ -200,7 +200,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
current_stock_qty = args.get(column)
elif args.get("return_qty_from_rejected_warehouse"):
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
current_stock_qty = (
args.get(column) * args.get("conversion_factor", 1.0)
if column != "stock_qty"
else args.get(column)
)
else:
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
@@ -845,13 +849,14 @@ def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
if not _bundle_ids:
return frappe._dict({})
return get_serial_batches_based_on_bundle(field, _bundle_ids)
return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids)
def get_serial_batches_based_on_bundle(field, _bundle_ids):
def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
available_dict = frappe._dict({})
batch_serial_nos = frappe.get_all(
"Serial and Batch Bundle",
@@ -863,6 +868,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`voucher_no`",
"`tabSerial and Batch Bundle`.`item_code`",
],
filters=[
["Serial and Batch Bundle", "name", "in", _bundle_ids],
@@ -876,6 +882,16 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
if doctype == "Packed Item":
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":
key = frappe.get_cached_value("Sales Invoice Item", key, "sales_invoice_item")
key = (row.item_code, key)
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
row.qty = -1 * row.qty
@@ -904,6 +920,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
if doctype == "Packed Item":
filters = get_filters_for_packed_item(field, reference_ids)
pluck_field = "serial_and_batch_bundle"
if is_rejected:
@@ -917,10 +935,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
pluck=pluck_field,
)
if _bundle_ids and doctype == "Packed Item":
return _bundle_ids
if not _bundle_ids:
return {}
del filters["name"]
if "name" in filters:
del filters["name"]
filters[field] = ("in", reference_ids)
@@ -963,10 +985,29 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
return _bundle_ids
def get_filters_for_packed_item(field, reference_ids):
names = []
filters = {"docstatus": 1, "dn_detail": ("in", reference_ids)}
if dns := frappe.get_all("Delivery Note Item", filters=filters, pluck="name"):
names.extend(dns)
filters = {"docstatus": 1, "sales_invoice_item": ("in", reference_ids)}
if sis := frappe.get_all("Sales Invoice Item", filters=filters, pluck="name"):
names.extend(sis)
if names:
reference_ids.extend(names)
return {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "stock_qty"
if not hasattr(row, qty_field):
qty_field = "qty"
if not warehouse_field:
warehouse_field = "warehouse"
@@ -1056,6 +1097,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
if not qty_field:
qty_field = "stock_qty"
if not hasattr(child_doc, qty_field):
qty_field = "qty"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):
warehouse = child_doc.get("target_warehouse")

View File

@@ -517,8 +517,15 @@ class SellingController(StockController):
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
item_details = frappe.get_cached_value(
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
get_valuation_method(d.item_code) == "Moving Average"
and self.get("is_return")
and not item_details.has_serial_no
and not item_details.has_batch_no
):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
@@ -997,6 +1004,9 @@ def set_default_income_account_for_item(obj):
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if parent.get("is_return") and parent.get("packed_items"):
return
if child.get("use_serial_batch_fields"):
return

View File

@@ -335,10 +335,20 @@ class StockController(AccountsController):
return
child_doctype = self.doctype + " Item"
if table_name == "packed_items":
field = "parent_detail_docname"
child_doctype = "Packed Item"
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
value = row.get(field)
if table_name == "packed_items" and row.get("parent_detail_docname"):
value = self.get_value_for_packed_item(row)
if not value:
continue
if data := available_dict.get(value):
data = filter_serial_batches(self, data, row)
bundle = make_serial_batch_bundle_for_return(data, row, self)
row.db_set(
@@ -354,6 +364,14 @@ class StockController(AccountsController):
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
)
def get_value_for_packed_item(self, row):
parent_items = self.get("items", {"name": row.parent_detail_docname})
if parent_items:
ref = parent_items[0].get("dn_detail")
return (row.item_code, ref)
return None
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
field = {
"Sales Invoice": "sales_invoice_item",
@@ -388,6 +406,12 @@ class StockController(AccountsController):
):
reference_ids.append(row.get(field))
if table_name == "packed_items" and row.get("parent_detail_docname"):
parent_rows = self.get("items", {"name": row.parent_detail_docname}) or []
for d in parent_rows:
if d.get(field) and not d.get(bundle_field):
reference_ids.append(d.get(field))
return field, reference_ids
@frappe.request_cache

View File

@@ -12,6 +12,8 @@ from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
get_available_serial_nos,
get_voucher_wise_serial_batch_from_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -52,9 +54,42 @@ class SubcontractingController(StockController):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.validate_items()
self.create_raw_materials_supplied()
self.set_valuation_rate_for_rm()
else:
super().validate()
def set_valuation_rate_for_rm(self):
rate_changed = False
if self.doctype == "Subcontracting Receipt":
for row in self.supplied_items:
kwargs = frappe._dict(
{
"item_code": row.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": flt(row.consumed_qty) * (-1 if not self.is_return else 1),
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
"serial_and_batch_bundle": row.serial_and_batch_bundle,
"voucher_detail_no": row.name,
"batch_no": row.batch_no,
"serial_no": row.serial_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
}
)
rate = get_incoming_rate(kwargs)
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
if flt(rate, precision) != flt(row.rate, precision):
row.rate = rate
row.amount = flt(row.consumed_qty) * flt(rate)
rate_changed = True
if rate_changed:
self.calculate_items_qty_and_amount()
def validate_rejected_warehouse(self):
for item in self.get("items"):
if flt(item.rejected_qty) and not item.rejected_warehouse:
@@ -166,6 +201,9 @@ class SubcontractingController(StockController):
self.set(self.raw_material_table, [])
return
if not self.get(self.raw_material_table):
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
@@ -610,6 +648,67 @@ class SubcontractingController(StockController):
self.set_rate_for_supplied_items(rm_obj, item_row)
elif self.backflush_based_on == "BOM":
self.update_rate_for_supplied_items()
self.set_batch_for_supplied_items()
def set_batch_for_supplied_items(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
from erpnext.stock.get_item_details import get_filtered_serial_nos
if self.is_return:
return
for row in self.supplied_items:
item_details = frappe.get_cached_value(
"Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if not item_details.has_batch_no and not item_details.has_serial_no:
continue
if not row.use_serial_batch_fields:
continue
kwargs = frappe._dict(
{
"item_code": row.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": flt(row.consumed_qty),
}
)
if item_details.has_serial_no and not row.serial_and_batch_bundle and not row.serial_no:
serial_nos = get_available_serial_nos(kwargs)
if serial_nos:
serial_nos = [sn.get("serial_no") for sn in serial_nos]
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
for index, d in enumerate(batches):
if consumed_qty <= 0:
break
if index == 0:
row.batch_no = d.get("batch_no")
row.consumed_qty = d.get("qty")
consumed_qty -= d.get("qty")
else:
new_row = self.append("supplied_items", {})
new_row.update(frappe.copy_doc(row).as_dict())
new_row.update(
{
"consumed_qty": d.get("qty"),
"batch_no": d.get("batch_no"),
"rate": row.rate,
"amount": flt(d.get("qty")) * flt(row.rate),
}
)
consumed_qty -= d.get("qty")
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":

View File

@@ -1308,6 +1308,7 @@ def make_subcontracted_items():
"Subcontracted Item SA7": {},
"Subcontracted Item SA8": {},
"Subcontracted Item SA9": {"stock_uom": "Litre"},
"Subcontracted Item SA10": {},
}
for item, properties in sub_contracted_items.items():
@@ -1329,6 +1330,7 @@ def make_raw_materials():
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
"Subcontracted SRM Item 8": {},
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
"Subcontracted SRM Item 10": {},
}
for item, properties in raw_materials.items():
@@ -1357,6 +1359,7 @@ def make_service_items():
"Subcontracted Service Item 7": {},
"Subcontracted Service Item 8": {},
"Subcontracted Service Item 9": {},
"Subcontracted Service Item 10": {},
}
for item, properties in service_items.items():
@@ -1381,6 +1384,7 @@ def make_bom_for_subcontracted_items():
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
"Subcontracted Item SA10": ["Subcontracted SRM Item 10"],
}
for item_code, raw_materials in boms.items():

View File

@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"code_list",
"canonical_uri",
"title",
"common_code",
"description",
@@ -71,10 +72,17 @@
"in_list_view": 1,
"label": "Description",
"max_height": "60px"
},
{
"fetch_from": "code_list.canonical_uri",
"fieldname": "canonical_uri",
"fieldtype": "Data",
"label": "Canonical URI"
}
],
"grid_page_length": 50,
"links": [],
"modified": "2024-11-06 07:46:17.175687",
"modified": "2025-10-04 17:22:28.176155",
"modified_by": "Administrator",
"module": "EDI",
"name": "Common Code",
@@ -94,10 +102,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "common_code,description",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -22,6 +22,7 @@ class CommonCode(Document):
additional_data: DF.Code | None
applies_to: DF.Table[DynamicLink]
canonical_uri: DF.Data | None
code_list: DF.Link
common_code: DF.Data
description: DF.SmallText | None

View File

@@ -253,6 +253,13 @@ class BOMCreator(Document):
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
key = (row.fg_item, row.fg_reference_id)
if key not in production_item_wise_rm:
production_item_wise_rm.setdefault(
key,
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
)
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))

View File

@@ -748,6 +748,7 @@ class ProductionPlan(Document):
work_order_data = {
"wip_warehouse": default_warehouses.get("wip_warehouse"),
"fg_warehouse": default_warehouses.get("fg_warehouse"),
"scrap_warehouse": default_warehouses.get("scrap_warehouse"),
"company": self.get("company"),
}
@@ -1631,7 +1632,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
}
)
sales_order = doc.get("sales_order")
sales_order = data.get("sales_order")
for item_code, details in item_details.items():
so_item_details.setdefault(sales_order, frappe._dict())
@@ -1821,7 +1822,7 @@ def get_sub_assembly_items(
def set_default_warehouses(row, default_warehouses):
for field in ["wip_warehouse", "fg_warehouse"]:
for field in ["wip_warehouse", "fg_warehouse", "scrap_warehouse"]:
if not row.get(field):
row[field] = default_warehouses.get(field)

View File

@@ -2828,6 +2828,111 @@ class TestWorkOrder(FrappeTestCase):
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
)
def test_req_qty_clamping_in_manufacture_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
fg_item = "Test Unconsumed RM FG Item"
rm_item_1 = "Test Unconsumed RM Item 1"
rm_item_2 = "Test Unconsumed RM Item 2"
source_warehouse = "_Test Warehouse - _TC"
wip_warehouse = "Stores - _TC"
fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
make_item(fg_item, {"is_stock_item": 1})
make_item(rm_item_1, {"is_stock_item": 1})
make_item(rm_item_2, {"is_stock_item": 1})
# create a BOM: 1 FG = 1 RM1 + 1 RM2
bom = make_bom(
item=fg_item,
source_warehouse=source_warehouse,
raw_materials=[rm_item_1, rm_item_2],
operating_cost_per_bom_quantity=1,
do_not_submit=True,
)
for row in bom.exploded_items:
make_stock_entry_test_record(
item_code=row.item_code,
target=source_warehouse,
qty=100,
basic_rate=100,
)
wo = make_wo_order_test_record(
item=fg_item,
qty=50,
source_warehouse=source_warehouse,
wip_warehouse=wip_warehouse,
)
wo.submit()
# first partial transfer & manufacture (6 units)
se_transfer_1 = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", 6, wip_warehouse)
)
se_transfer_1.insert()
se_transfer_1.submit()
stock_entry_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 6, fg_warehouse))
# remove rm_2 from the items to simulate unconsumed RM scenario
stock_entry_1.items = [row for row in stock_entry_1.items if row.item_code != rm_item_2]
stock_entry_1.save()
stock_entry_1.submit()
wo.reload()
se_transfer_2 = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", 20, wip_warehouse)
)
se_transfer_2.insert()
se_transfer_2.submit()
stock_entry_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 20, fg_warehouse))
# validate rm_item_2 quantity is clamped correctly (per-unit BOM = 1 → max 20)
for row in stock_entry_2.items:
if row.item_code == rm_item_2:
self.assertLessEqual(row.qty, 20)
self.assertGreaterEqual(row.qty, 0)
def test_overproduction_allowed_qty(self):
"""Test overproduction allowed qty in work order"""
allow_overproduction("overproduction_percentage_for_work_order", 50)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=10)
test_stock_entry.make_stock_entry(
item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100
)
test_stock_entry.make_stock_entry(
item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC",
qty=100,
basic_rate=1000.0,
)
mt_stock_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 10)
)
mt_stock_entry.submit()
fg_stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
fg_stock_entry.items[2].qty = 15
fg_stock_entry.fg_completed_qty = 15
fg_stock_entry.submit()
wo_order.reload()
self.assertEqual(wo_order.produced_qty, 15)
self.assertEqual(wo_order.status, "Completed")
allow_overproduction("overproduction_percentage_for_work_order", 0)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -263,6 +263,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta
erpnext.patches.v14_0.update_proprietorship_to_individual
erpnext.patches.v15_0.rename_subcontracting_fields
erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage
erpnext.patches.v16_0.create_company_custom_fields
[post_model_sync]
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets

View File

@@ -9,5 +9,9 @@ def execute():
docs = frappe.get_all(
"Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name"
)
for doc in docs:
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)
frappe.db.auto_commit_on_many_writes = 1
try:
for doc in docs:
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)
finally:
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -0,0 +1,6 @@
from erpnext.setup.install import create_custom_company_links
def execute():
"""Add link fields to Company in Email Account and Communication."""
create_custom_company_links()

View File

@@ -173,8 +173,11 @@ erpnext.buying = {
callback: (r) => {
this.frm.set_value("billing_address", r.message.primary_address || "");
if(!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
this.frm.set_value("shipping_address", r.message.shipping_address || "");
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
this.frm.set_value(
"shipping_address",
r.message.shipping_address || this.frm.doc.shipping_address || ""
);
},
});
erpnext.utils.set_letter_head(this.frm)

View File

@@ -76,9 +76,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Update paid amount on return/debit note creation
if (
this.frm.doc.doctype === "Purchase Invoice"
&& this.frm.doc.is_return
&& (this.frm.doc.grand_total > this.frm.doc.paid_amount)
this.frm.doc.doctype === "Purchase Invoice" &&
this.frm.doc.is_return &&
this.frm.doc.grand_total < 0 &&
this.frm.doc.grand_total > this.frm.doc.paid_amount
) {
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
}

View File

@@ -1082,12 +1082,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
due_date(doc, cdt) {
discount_date(doc, cdt, cdn) {
// Remove fields as discount_date is auto-managed by payment terms
const row = locals[cdt][cdn];
["discount_validity", "discount_validity_based_on"].forEach((field) => {
row[field] = "";
});
this.frm.refresh_field("payment_schedule");
}
due_date(doc, cdt, cdn) {
// due_date is to be changed, payment terms template and/or payment schedule must
// be removed as due_date is automatically changed based on payment terms
if (doc.doctype !== cdt) {
// triggered by change to the due_date field in payment schedule child table
// do nothing to avoid infinite clearing loop
// Remove fields as due_date is auto-managed by payment terms
const row = locals[cdt][cdn];
["due_date_based_on", "credit_days", "credit_months"].forEach((field) => {
row[field] = "";
});
this.frm.refresh_field("payment_schedule");
return;
}
@@ -2137,16 +2150,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.doc.name).options,
"master_name": this.frm.doc.taxes_and_charges
},
callback: function(r) {
if(!r.exc) {
if(me.frm.doc.shipping_rule && me.frm.doc.taxes) {
for (let tax of r.message) {
callback: function (r) {
if (!r.exc) {
let taxes = r.message;
taxes.forEach((tax) => {
if (me.frm.doc?.cost_center && !tax.cost_center) {
tax.cost_center = me.frm.doc.cost_center;
}
});
if (me.frm.doc.shipping_rule && me.frm.doc.taxes) {
for (let tax of taxes) {
me.frm.add_child("taxes", tax);
}
refresh_field("taxes");
} else {
me.frm.set_value("taxes", r.message);
me.frm.set_value("taxes", taxes);
me.calculate_taxes_and_totals();
}
}
@@ -2573,6 +2592,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
'item_group': item.item_group,
"base_net_rate": item.base_net_rate,
"disabled": 0,
}
if (doc.tax_category)
@@ -2614,6 +2634,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
payment_term(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn];
// empty date condition fields
[
"due_date_based_on",
"credit_days",
"credit_months",
"discount_validity",
"discount_validity_based_on",
].forEach(function (field) {
row[field] = "";
});
if(row.payment_term) {
frappe.call({
method: "erpnext.controllers.accounts_controller.get_payment_term_details",
@@ -2626,14 +2657,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if(r.message && !r.exc) {
for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]);
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
const company_currency = me.get_company_currency();
for (let d in r.message) {
row[d] = r.message[d];
}
me.update_payment_schedule_grid_labels(company_currency)
me.frm.refresh_field("payment_schedule");
}
}
})
},
});
} else {
me.frm.refresh_field("payment_schedule");
}
}

View File

@@ -443,3 +443,14 @@ def create_internal_customer(customer_name=None, represents_company=None, allowe
customer_name = frappe.db.get_value("Customer", customer_name)
return customer_name
def make_customer(customer_name):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.customer_type = "Individual"
customer.insert()
return customer.name
else:
return customer_name

View File

@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_maintenance_schedule,
)
@@ -1680,14 +1680,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
so.load_from_db()
self.assertRaises(frappe.LinkExistsError, so.cancel)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
automatically_fetch_payment_terms()
so = make_sales_order(uom="Nos", do_not_save=1)
create_payment_terms_template()
so.payment_terms_template = "Test Receivable Template"
@@ -1701,8 +1700,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
compare_payment_schedules(self, so, si)
automatically_fetch_payment_terms(enable=0)
def test_zero_amount_sales_order_billing_status(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -2421,16 +2418,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(si2.items[0].qty, 20)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
accounts_settings.save()
def compare_payment_schedules(doc, doc1, doc2):
for index, schedule in enumerate(doc1.get("payment_schedule")):
posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date")
due_date = schedule.due_date
if schedule.due_date_based_on:
due_date = get_due_date(schedule, posting_date=posting_date)
doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term)
doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date)
doc.assertEqual(due_date, doc2.payment_schedule[index].due_date)
doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion)
doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount)

View File

@@ -17,6 +17,7 @@
"role_to_override_stop_action",
"column_break_15",
"maintain_same_sales_rate",
"fallback_to_default_price_list",
"editable_price_list_rate",
"validate_selling_price",
"editable_bundle_item_rates",
@@ -216,6 +217,12 @@
"fieldname": "allow_zero_qty_in_quotation",
"fieldtype": "Check",
"label": "Allow Quotation with Zero Quantity"
},
{
"default": "0",
"fieldname": "fallback_to_default_price_list",
"fieldtype": "Check",
"label": "Use Prices from Default Price List as Fallback"
}
],
"grid_page_length": 50,
@@ -224,7 +231,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-06 15:23:14.332971",
"modified": "2025-09-23 21:10:14.826653",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
@@ -33,6 +34,7 @@ class SellingSettings(Document):
editable_bundle_item_rates: DF.Check
editable_price_list_rate: DF.Check
enable_discount_accounting: DF.Check
fallback_to_default_price_list: DF.Check
hide_tax_id: DF.Check
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
maintain_same_sales_rate: DF.Check
@@ -69,16 +71,35 @@ class SellingSettings(Document):
hide_name_field=False,
)
self.validate_fallback_to_default_price_list()
def validate_fallback_to_default_price_list(self):
if (
self.fallback_to_default_price_list
and self.has_value_changed("fallback_to_default_price_list")
and frappe.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
):
stock_meta = frappe.get_meta("Stock Settings")
frappe.msgprint(
_(
"You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted into the transaction price list."
).format(
"<i>{}</i>".format(_(self.meta.get_label("fallback_to_default_price_list"))),
"<i>{}</i>".format(_(stock_meta.get_label("auto_insert_price_list_rate_if_missing"))),
frappe.bold(_("Stock Settings")),
)
)
def toggle_hide_tax_id(self):
self.hide_tax_id = cint(self.hide_tax_id)
_hide_tax_id = cint(self.hide_tax_id)
# Make property setters to hide tax_id fields
for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"):
make_property_setter(
doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False
doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False
)
make_property_setter(
doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False
doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False
)
def toggle_editable_rate_for_bundle_items(self):

View File

@@ -25,6 +25,7 @@ def after_install():
set_single_defaults()
create_print_setting_custom_fields()
create_custom_company_links()
add_all_roles_to("Administrator")
create_default_success_action()
create_default_energy_point_rules()
@@ -132,6 +133,39 @@ def create_print_setting_custom_fields():
)
def create_custom_company_links():
"""Add link fields to Company in Email Account and Communication.
These DocTypes are provided by the Frappe Framework but need to be associated
with a company in ERPNext to allow for multitenancy. I.e. one company should
not be able to access emails and communications from another company.
"""
create_custom_fields(
{
"Email Account": [
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"insert_after": "email_id",
},
],
"Communication": [
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"insert_after": "email_account",
"fetch_from": "email_account.company",
"read_only": 1,
},
],
},
)
def create_default_success_action():
for success_action in get_default_success_action():
if not frappe.db.exists("Success Action", success_action.get("ref_doctype")):

View File

@@ -7,15 +7,16 @@ from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt, nowtime
from frappe.utils.deprecations import deprecated
from pypika import Order
from pypika.functions import Coalesce
class DeprecatedSerialNoValuation:
@deprecated
def calculate_stock_value_from_deprecarated_ledgers(self):
if not has_sle_for_serial_nos(self.sle.item_code):
return
serial_nos = []
if hasattr(self, "old_serial_nos"):
serial_nos = self.old_serial_nos
serial_nos = self.get_filterd_serial_nos()
if not serial_nos:
return
@@ -25,17 +26,6 @@ class DeprecatedSerialNoValuation:
self.stock_value_change += flt(stock_value_change)
def get_filterd_serial_nos(self):
serial_nos = []
non_filtered_serial_nos = self.get_serial_nos()
# If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered
for serial_no in non_filtered_serial_nos:
if serial_no and serial_no not in self.serial_no_incoming_rate:
serial_nos.append(serial_no)
return serial_nos
@deprecated
def get_incoming_value_for_serial_nos(self, serial_nos):
from erpnext.stock.utils import get_combine_datetime
@@ -81,20 +71,6 @@ class DeprecatedSerialNoValuation:
return incoming_values
@frappe.request_cache
def has_sle_for_serial_nos(item_code):
serial_nos = frappe.db.get_all(
"Stock Ledger Entry",
fields=["name"],
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": item_code},
limit=1,
)
if serial_nos:
return True
return False
class DeprecatedBatchNoValuation:
@deprecated
def calculate_avg_rate_from_deprecarated_ledgers(self):
@@ -197,9 +173,15 @@ class DeprecatedBatchNoValuation:
@deprecated
def set_balance_value_for_non_batchwise_valuation_batches(self):
self.last_sle = self.get_last_sle_for_non_batch()
if hasattr(self, "prev_sle"):
self.last_sle = self.prev_sle
else:
self.last_sle = self.get_last_sle_for_non_batch()
if self.last_sle and self.last_sle.stock_queue:
self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or []
self.stock_queue = self.last_sle.stock_queue
if isinstance(self.stock_queue, str):
self.stock_queue = json.loads(self.stock_queue) or []
self.set_balance_value_from_sl_entries()
self.set_balance_value_from_bundle()
@@ -293,10 +275,7 @@ class DeprecatedBatchNoValuation:
query = query.where(sle.name != self.sle.name)
if self.sle.serial_and_batch_bundle:
query = query.where(
(sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle)
| (sle.serial_and_batch_bundle.isnull())
)
query = query.where(Coalesce(sle.serial_and_batch_bundle, "") != self.sle.serial_and_batch_bundle)
data = query.run(as_dict=True)

View File

@@ -22,6 +22,17 @@ frappe.ui.form.on("Batch", {
frappe.set_route("query-report", "Stock Ledger");
});
frm.trigger("make_dashboard");
frm.add_custom_button(__("Recalculate Batch Qty"), () => {
frm.call({
method: "recalculate_batch_qty",
doc: frm.doc,
freeze: true,
callback: () => {
frm.reload_doc();
},
});
});
}
},
item: (frm) => {

View File

@@ -156,6 +156,17 @@ class Batch(Document):
if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
frappe.throw(_("The selected item cannot have Batch"))
@frappe.whitelist()
def recalculate_batch_qty(self):
batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True)
batch_qty = 0.0
if batches:
for row in batches:
batch_qty += row.get("qty")
self.db_set("batch_qty", batch_qty)
frappe.msgprint(_("Batch Qty updated to {0}").format(batch_qty), alert=True)
def set_batchwise_valuation(self):
from erpnext.stock.utils import get_valuation_method

View File

@@ -15,7 +15,6 @@ from erpnext.accounts.utils import get_balance_on
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
compare_payment_schedules,
create_dn_against_so,
make_sales_order,
@@ -1300,14 +1299,13 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
automatically_fetch_payment_terms()
so = make_sales_order(uom="Nos", do_not_save=1)
create_payment_terms_template()
so.payment_terms_template = "Test Receivable Template"
@@ -1327,8 +1325,6 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
compare_payment_schedules(self, so, si)
automatically_fetch_payment_terms(enable=0)
def test_returned_qty_in_return_dn(self):
# SO ---> SI ---> DN
# |
@@ -2600,6 +2596,129 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.per_returned, 100)
def test_sales_return_for_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
from erpnext.stock.doctype.item.test_item import make_item
rm_items = []
for item_code, properties in {
"_Packed Service Item": {"is_stock_item": 0},
"_Packed FG Item New 1": {
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SN-PACKED-1-.#####",
},
"_Packed FG Item New 2": {
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-PACKED-2-.#####",
},
"_Packed FG Item New 3": {
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-PACKED-3-.#####",
"has_serial_no": 1,
"serial_no_series": "SN-PACKED-3-.#####",
},
}.items():
if not frappe.db.exists("Item", item_code):
make_item(item_code, properties)
if item_code != "_Packed Service Item":
rm_items.append(item_code)
for rate in [100, 200]:
make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate)
make_product_bundle("_Packed Service Item", rm_items)
dn = create_delivery_note(
item_code="_Packed Service Item",
warehouse="_Test Warehouse - _TC",
qty=5,
)
dn.reload()
serial_batch_map = {}
for row in dn.packed_items:
self.assertTrue(row.serial_and_batch_bundle)
if row.item_code not in serial_batch_map:
serial_batch_map[row.item_code] = frappe._dict(
{
"serial_nos": [],
"batches": defaultdict(int),
"serial_no_valuation": defaultdict(float),
"batch_no_valuation": defaultdict(float),
}
)
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
serial_batch_map[row.item_code].serial_nos.append(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate
if entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate
dn1 = make_sales_return(dn.name)
dn1.items[0].qty = -2
dn1.submit()
dn1.reload()
for row in dn1.packed_items:
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
)
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
elif entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 2.0)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
)
dn2 = make_sales_return(dn.name)
dn2.items[0].qty = -3
dn2.submit()
dn2.reload()
for row in dn2.packed_items:
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
)
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
elif entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0)
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 3.0)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -140,7 +140,6 @@ frappe.ui.form.on("Pick List", {
frm.trigger("add_get_items_button");
if (frm.doc.docstatus === 1) {
const status_completed = frm.doc.status === "Completed";
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
if (!status_completed) {
frm.add_custom_button(__("Update Current Stock"), () =>

View File

@@ -77,7 +77,6 @@
"options": "Work Order"
},
{
"allow_on_submit": 1,
"fieldname": "locations",
"fieldtype": "Table",
"label": "Item Locations",
@@ -247,7 +246,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2025-07-23 08:34:32.099673",
"modified": "2025-10-03 18:36:52.282355",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@@ -74,8 +74,11 @@ class PickList(TransactionBase):
if self.has_reserved_stock():
self.set_onload("has_reserved_stock", True)
for item in self.get("locations"):
item.update(get_item_details(item.item_code, item.uom, item.warehouse, self.company))
if self.docstatus.is_draft() and not hasattr(self, "_action"):
company = self.company
for item in self.get("locations"):
item.update(get_item_details(item.item_code, item.uom, item.warehouse, company))
def validate(self):
self.validate_expired_batches()
@@ -1446,7 +1449,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
@frappe.whitelist()
def get_item_details(item_code, uom=None, warehouse=None, company=None):
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
details = frappe.db.get_value("Item", item_code, "stock_uom", as_dict=1)
details.uom = uom or details.stock_uom
if uom:
details.update(get_conversion_factor(item_code, uom))

View File

@@ -1180,6 +1180,7 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1190,12 +1191,9 @@ class TestPurchaseReceipt(FrappeTestCase):
make_pr_against_po,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
compare_payment_schedules,
)
automatically_fetch_payment_terms()
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
create_payment_terms_template()
po.payment_terms_template = "Test Receivable Template"
@@ -1213,8 +1211,6 @@ class TestPurchaseReceipt(FrappeTestCase):
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi)
automatically_fetch_payment_terms(enable=0)
@change_settings("Stock Settings", {"allow_negative_stock": 1})
def test_neg_to_positive(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

View File

@@ -311,7 +311,7 @@ class SerialandBatchBundle(Document):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False):
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False, prev_sle=None):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
@@ -321,15 +321,15 @@ class SerialandBatchBundle(Document):
return
if return_against := self.get_return_against(parent=parent):
self.set_valuation_rate_for_return_entry(return_against, row, save)
self.set_valuation_rate_for_return_entry(return_against, row, save, prev_sle=prev_sle)
elif self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
)
else:
self.set_incoming_rate_for_inward_transaction(row, save)
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def set_valuation_rate_for_return_entry(self, return_against, row, save=False):
def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None):
if valuation_details := self.get_valuation_rate_for_return_entry(return_against):
for row in self.entries:
if valuation_details:
@@ -361,7 +361,7 @@ class SerialandBatchBundle(Document):
)
elif self.type_of_transaction == "Inward":
self.set_incoming_rate_for_inward_transaction(row, save)
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
if frappe.flags.through_repost_item_valuation:
@@ -529,7 +529,11 @@ class SerialandBatchBundle(Document):
if save:
d.db_set(
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
{
"incoming_rate": d.incoming_rate,
"stock_value_difference": d.stock_value_difference,
"stock_queue": d.get("stock_queue"),
}
)
def validate_negative_batch(self, batch_no, available_qty):
@@ -606,7 +610,11 @@ class SerialandBatchBundle(Document):
return return_against
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None):
from erpnext.stock.utils import get_valuation_method
valuation_method = get_valuation_method(self.item_code)
valuation_field = "valuation_rate"
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
valuation_field = "incoming_rate"
@@ -630,19 +638,56 @@ class SerialandBatchBundle(Document):
if not rate and self.voucher_detail_no and self.voucher_no:
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
is_packed_item = False
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
rate = frappe.db.get_value(
"Packed Item",
self.voucher_detail_no,
"incoming_rate",
)
if rate is not None:
is_packed_item = True
stock_queue = []
batches = []
if prev_sle and prev_sle.stock_queue:
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
for d in self.entries:
if self.is_rejected:
rate = 0.0
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference:
continue
if is_packed_item and d.incoming_rate:
rate = d.incoming_rate
d.incoming_rate = flt(rate)
if d.qty:
d.stock_value_difference = flt(d.qty) * d.incoming_rate
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
stock_queue.append([d.qty, d.incoming_rate])
d.stock_queue = json.dumps(stock_queue)
if save:
d.db_set(
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
{
"incoming_rate": d.incoming_rate,
"stock_value_difference": d.stock_value_difference,
"stock_queue": d.get("stock_queue"),
}
)
def set_serial_and_batch_values(self, parent, row, qty_field=None):

View File

@@ -285,6 +285,25 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
se = make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
qty=10,
rate=100,
batch_no=batch_id,
use_serial_batch_fields=True,
)
sle = frappe.db.get_value(
"Serial and Batch Entry",
{"parent": se.items[0].serial_and_batch_bundle, "docstatus": 1},
["stock_value_difference", "stock_queue"],
as_dict=True,
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
se = make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
@@ -301,7 +320,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
se = make_stock_entry(
item_code=batch_item_code,
@@ -319,6 +338,24 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertEqual(flt(sle.stock_value_difference), 5000.00 * -1)
self.assertFalse(json.loads(sle.stock_queue or "[]"))
self.assertEqual(flt(sle.stock_value), 1000.0)
se = make_stock_entry(
item_code=batch_item_code,
source="_Test Warehouse - _TC",
qty=10,
use_serial_batch_fields=False,
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
["stock_value_difference", "stock_queue", "stock_value"],
as_dict=True,
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
self.assertFalse(json.loads(sle.stock_queue or "[]"))
self.assertEqual(flt(sle.stock_value), 0.0)
def test_old_serial_no_valuation(self):

View File

@@ -823,9 +823,28 @@ frappe.ui.form.on("Stock Entry", {
refresh_field("process_loss_qty");
}
},
set_fg_completed_qty(frm) {
let fg_completed_qty = 0;
frm.doc.items.forEach((item) => {
if (item.is_finished_item) {
fg_completed_qty += flt(item.qty);
}
});
frm.doc.fg_completed_qty = fg_completed_qty;
frm.refresh_field("fg_completed_qty");
},
});
frappe.ui.form.on("Stock Entry Detail", {
items_add(frm, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
},
set_basic_rate_manually(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frm.fields_dict.items.grid.update_docfield_property(
@@ -837,6 +856,10 @@ frappe.ui.form.on("Stock Entry Detail", {
qty(frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
},
conversion_factor(frm, cdt, cdn) {

View File

@@ -1356,12 +1356,6 @@ class StockEntry(StockController):
d.item_code, self.work_order
)
)
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
frappe.throw(
_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format(
d.idx, d.transfer_qty, self.fg_completed_qty
)
)
finished_items.append(d.item_code)
@@ -2041,6 +2035,9 @@ class StockEntry(StockController):
# in case of BOM
to_warehouse = item.get("default_warehouse")
expense_account = item.get("expense_account")
if not expense_account:
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
@@ -2048,7 +2045,7 @@ class StockEntry(StockController):
"item_name": item.item_name,
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"expense_account": expense_account,
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1,
}
@@ -2276,10 +2273,12 @@ class StockEntry(StockController):
wo_item_qty = item.transferred_qty or item.required_qty
wo_qty_consumed = flt(wo_item_qty) - flt(item.consumed_qty)
wo_qty_unconsumed = flt(wo_item_qty) - flt(item.consumed_qty)
wo_qty_to_produce = flt(work_order_qty) - flt(wo.produced_qty)
bom_qty_per_unit = item.required_qty / wo.qty # per-unit BOM qty
req_qty_each = (wo_qty_consumed) / (wo_qty_to_produce or 1)
req_qty_each = (wo_qty_unconsumed) / (wo_qty_to_produce or 1)
req_qty_each = min(req_qty_each, bom_qty_per_unit)
qty = req_qty_each * flt(self.fg_completed_qty)

View File

@@ -1323,9 +1323,18 @@ class TestStockEntry(FrappeTestCase):
posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer",
),
dict(
item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer",
),
]
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self):

View File

@@ -73,7 +73,8 @@
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Data",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "warehouse",
@@ -361,7 +362,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-04-22 12:37:41.304109",
"modified": "2025-10-04 09:59:15.546556",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -84,7 +84,7 @@ def get_reposting_entries():
def get_stock_ledgers(vouchers):
return frappe.get_all(
"Stock Ledger Entry",
fields=["item_code", "warehouse", "posting_date"],
fields=["item_code", "warehouse", "posting_date", "posting_time", "posting_datetime"],
filters={"voucher_no": ("in", vouchers)},
)

View File

@@ -105,6 +105,7 @@ class StockSettings(Document):
self.validate_clean_description_html()
self.validate_pending_reposts()
self.validate_stock_reservation()
self.validate_auto_insert_price_list_rate_if_missing()
self.change_precision_for_for_sales()
self.change_precision_for_purchase()
@@ -219,6 +220,23 @@ class StockSettings(Document):
)
)
def validate_auto_insert_price_list_rate_if_missing(self):
if (
self.auto_insert_price_list_rate_if_missing
and self.has_value_changed("auto_insert_price_list_rate_if_missing")
and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list")
):
selling_meta = frappe.get_meta("Selling Settings")
frappe.msgprint(
_(
"You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list."
).format(
"<i>{}</i>".format(_(self.meta.get_label("auto_insert_price_list_rate_if_missing"))),
"<i>{}</i>".format(_(selling_meta.get_label("fallback_to_default_price_list"))),
frappe.bold(_("Selling Settings")),
)
)
def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer()

View File

@@ -98,6 +98,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
out.update(get_price_list_rate(args, item))
if (
not out.price_list_rate
and args.transaction_type == "selling"
and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list")
):
fallback_args = args.copy()
fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list")
out.update(get_price_list_rate(fallback_args, item))
args.customer = current_customer
if args.customer and cint(args.is_pos):
@@ -260,10 +269,13 @@ def filter_batches(batches, doc):
del batches[row.get("batch_no")]
def get_filtered_serial_nos(serial_nos, doc):
def get_filtered_serial_nos(serial_nos, doc, table=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for row in doc.get("items"):
if not table:
table = "items"
for row in doc.get(table):
if row.get("serial_no"):
for serial_no in get_serial_nos(row.get("serial_no")):
if serial_no in serial_nos:
@@ -692,8 +704,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
taxes_with_no_validity = []
for tax in taxes:
tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company")
if tax_company == args["company"]:
disabled, tax_company = frappe.get_cached_value(
"Item Tax Template", tax.item_tax_template, ["disabled", "company"]
)
if not disabled and tax_company == args["company"]:
if tax.valid_from or tax.maximum_net_rate:
# In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date
@@ -1504,7 +1518,7 @@ def get_valuation_rate(item_code, company, warehouse=None):
return frappe.db.get_value(
"Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True
) or {"valuation_rate": 0}
) or {"valuation_rate": item.get("valuation_rate") or 0}
elif not item.get("is_stock_item"):
pi_item = frappe.qb.DocType("Purchase Invoice Item")

View File

@@ -4,10 +4,11 @@
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
from frappe.utils import get_datetime, get_link_to_form, parse_json
import erpnext
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
from erpnext.stock.doctype.stock_reposting_settings.stock_reposting_settings import get_stock_ledgers
from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_account
@@ -141,18 +142,29 @@ def create_reposting_entries(rows, company):
rows = parse_json(rows)
entries = []
for row in rows:
row = frappe._dict(row)
item_wh = frappe._dict()
vouchers = [row.get("voucher_no") for row in rows]
sles = get_stock_ledgers(vouchers)
for sle in sles:
key = (sle.item_code, sle.warehouse)
if key not in item_wh:
item_wh[key] = sle
elif get_datetime(item_wh.get(key).posting_datetime) > get_datetime(sle.posting_datetime):
item_wh[key] = sle
for key, sle in item_wh.items():
item_code, warehouse = key
try:
doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Transaction",
"based_on": "Item and Warehouse",
"status": "Queued",
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"posting_date": row.posting_date,
"item_code": item_code,
"warehouse": warehouse,
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
"company": company,
"allow_nagative_stock": 1,
}

View File

@@ -24,6 +24,11 @@ frappe.query_reports["Stock Qty vs Serial No Count"] = {
},
reqd: 1,
},
{
fieldname: "show_disables_items",
label: __("Show Disabled Items"),
fieldtype: "Check",
},
],
formatter: function (value, row, column, data, default_formatter) {

View File

@@ -9,7 +9,7 @@ from frappe import _
def execute(filters=None):
validate_warehouse(filters)
columns = get_columns()
data = get_data(filters.warehouse)
data = get_data(filters.warehouse, filters.show_disables_items)
return columns, data
@@ -38,12 +38,13 @@ def get_columns():
return columns
def get_data(warehouse):
def get_data(warehouse, show_disables_items):
filters = {"has_serial_no": True}
if not show_disables_items:
filters["disabled"] = False
serial_item_list = frappe.get_all(
"Item",
filters={
"has_serial_no": True,
},
filters=filters,
fields=["item_code", "item_name"],
)

View File

@@ -209,7 +209,20 @@ class SerialBatchBundle:
elif sn_doc.has_batch_no and len(sn_doc.entries) == 1:
values_to_update["batch_no"] = sn_doc.entries[0].batch_no
frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
if self.child_doctype == "Packed Item":
name = frappe.db.get_value(
"Packed Item",
{
"parent_detail_docname": sn_doc.voucher_detail_no,
"item_code": self.sle.item_code,
"serial_and_batch_bundle": ("is", "not set"),
},
"name",
)
frappe.db.set_value(self.child_doctype, name, values_to_update)
else:
frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
@property
def child_doctype(self):
@@ -227,6 +240,13 @@ class SerialBatchBundle:
if self.sle.voucher_type == "Asset Repair":
child_doctype = "Asset Repair Consumed Item"
if self.sle.voucher_type in ["Delivery Note", "Sales Invoice"] and self.sle.voucher_detail_no:
if (
frappe.db.get_value(self.sle.voucher_type + " Item", self.sle.voucher_detail_no, "item_code")
!= self.sle.item_code
):
child_doctype = "Packed Item"
return child_doctype
def is_rejected_entry(self):
@@ -580,11 +600,13 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
else:
self.serial_no_incoming_rate = defaultdict(float)
self.stock_value_change = 0.0
self.old_serial_nos = []
serial_nos = self.get_serial_nos()
for serial_no in serial_nos:
incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
if incoming_rate is None:
self.old_serial_nos.append(serial_no)
continue
self.stock_value_change += incoming_rate
@@ -1227,7 +1249,7 @@ class SerialBatchCreation:
def create_batch(self):
from erpnext.stock.doctype.batch.batch import make_batch
if self.is_rejected:
if hasattr(self, "is_rejected") and self.is_rejected:
bundle = frappe.db.get_value(
"Serial and Batch Bundle",
{
@@ -1363,18 +1385,19 @@ def get_batch_current_qty(batch):
def throw_negative_batch_validation(batch_no, qty):
frappe.throw(
_("The Batch {0} has negative quantity {1}. Please correct the quantity.").format(
bold(batch_no), bold(qty)
),
title=_("Negative Batch Quantity"),
frappe.msgprint(
_(
"The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry."
).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
title=_("Warning!"),
indicator="orange",
)
def get_batchwise_qty(voucher_type, voucher_no):
bundles = frappe.get_all(
"Serial and Batch Bundle",
filters={"voucher_no": voucher_no, "voucher_type": voucher_type},
filters={"voucher_no": voucher_no, "voucher_type": voucher_type, "docstatus": (">", 0)},
pluck="name",
)
if not bundles:

View File

@@ -725,32 +725,15 @@ class update_entries_after:
self.distinct_item_warehouses[key] = val
self.new_items_found = True
else:
# Check if the dependent voucher is reposted
# If not, then do not add it to the list
if not self.is_dependent_voucher_reposted(dependant_sle):
return
existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set(
dependent_voucher_detail_nos
):
return
val.sle_changed = True
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
existing_sle = self.distinct_item_warehouses[key].get("sle", {})
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
# Future dependent voucher needs to be repost to get the correct stock value
# If dependent voucher has not reposted, then add it to the list
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
self.new_items_found = True
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no
):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
def is_dependent_voucher_reposted(self, dependant_sle) -> bool:
# Return False if the dependent voucher is not reposted
@@ -1027,16 +1010,17 @@ class update_entries_after:
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
return
if self.args.get("sle_id") and sle.actual_qty < 0:
doc = frappe.db.get_value(
"Serial and Batch Bundle",
sle.serial_and_batch_bundle,
["total_amount", "total_qty"],
as_dict=1,
)
if sle.actual_qty < 0 and (
sle.voucher_type in ["Stock Reconciliation", "Asset Capitalization"]
or not frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_return")
):
doc = frappe._dict({})
self.update_serial_batch_no_valuation(sle, doc, prev_sle=self.wh_data)
else:
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.set_incoming_rate(
save=True, allow_negative_stock=self.allow_negative_stock, prev_sle=self.wh_data
)
doc.calculate_qty_and_amount(save=True)
if stock_queue := frappe.get_all(
@@ -1055,6 +1039,87 @@ class update_entries_after:
self.wh_data.qty_after_transaction, self.flt_precision
)
def update_serial_batch_no_valuation(self, sle, doc, prev_sle=None):
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
sabb_data = get_serial_from_sabb(sle.serial_and_batch_bundle)
if not sabb_data:
doc.update({"total_amount": 0.0, "total_qty": 0.0, "avg_rate": 0.0})
return
serial_nos = [d.serial_no for d in sabb_data if d.serial_no]
if serial_nos:
sle["serial_nos"] = get_serial_nos_data(",".join(serial_nos))
sn_obj = SerialNoValuation(
sle=sle,
item_code=self.item_code,
warehouse=sle.warehouse,
)
else:
sle["batch_nos"] = {row.batch_no: row for row in sabb_data if row.batch_no}
sn_obj = BatchNoValuation(
sle=sle,
item_code=self.item_code,
warehouse=sle.warehouse,
prev_sle=prev_sle,
)
tot_amt = 0.0
total_qty = 0.0
avg_rate = 0.0
for d in sabb_data:
incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj)
amount = incoming_rate * flt(d.qty)
tot_amt += flt(amount)
total_qty += flt(d.qty)
if flt(incoming_rate, self.currency_precision) == flt(
d.incoming_rate, self.currency_precision
) and not getattr(d, "stock_queue", None):
continue
values_to_update = {
"incoming_rate": incoming_rate,
"stock_value_difference": amount,
}
if d.stock_queue:
values_to_update["stock_queue"] = d.stock_queue
frappe.db.set_value(
"Serial and Batch Entry",
d.name,
values_to_update,
update_modified=False,
)
if total_qty:
avg_rate = tot_amt / total_qty
doc.update(
{
"total_amount": tot_amt,
"total_qty": total_qty,
"avg_rate": avg_rate,
}
)
frappe.db.set_value(
"Serial and Batch Bundle",
sle.serial_and_batch_bundle,
{
"total_qty": total_qty,
"avg_rate": avg_rate,
"total_amount": tot_amt,
},
update_modified=False,
)
for key in ("serial_nos", "batch_nos"):
if key in sle:
del sle[key]
def get_outgoing_rate_for_batched_item(self, sle):
if self.wh_data.qty_after_transaction == 0:
return 0
@@ -1772,6 +1837,8 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
"posting_time",
"voucher_detail_no",
"posting_datetime as timestamp",
"voucher_type",
"voucher_no",
],
as_dict=1,
)
@@ -2303,3 +2370,52 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
difference_amount = query.run()
return flt(difference_amount[0][0]) if difference_amount else 0
@frappe.request_cache
def is_transfer_stock_entry(voucher_no):
purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose")
return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"]
@frappe.request_cache
def get_serial_from_sabb(serial_and_batch_bundle):
return frappe.get_all(
"Serial and Batch Entry",
filters={"parent": serial_and_batch_bundle},
fields=["serial_no", "batch_no", "name", "qty", "incoming_rate"],
order_by="idx",
)
def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
if row.serial_no:
return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0))
else:
stock_queue = []
if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue:
stock_queue = parse_json(sn_obj.stock_queue)
val_method = get_valuation_method(item_code)
actual_qty = row.qty
if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches:
if actual_qty < 0:
stock_queue = FIFOValuation(stock_queue)
_prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
stock_queue.remove_stock(qty=abs(actual_qty))
_qty, stock_value = stock_queue.get_total_stock_and_value()
stock_value_difference = stock_value - prev_stock_value
incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty)))
stock_queue = stock_queue.state
else:
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
stock_queue.append([row.qty, incoming_rate])
row.stock_queue = json.dumps(stock_queue)
else:
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
return incoming_rate

View File

@@ -252,14 +252,18 @@ class SubcontractingOrder(SubcontractingController):
if si.fg_item:
item = frappe.get_doc("Item", si.fg_item)
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
available_qty = po_item.qty - po_item.subcontracted_quantity
qty, subcontracted_quantity, fg_item_qty = frappe.db.get_value(
"Purchase Order Item",
si.purchase_order_item,
["qty", "subcontracted_quantity", "fg_item_qty"],
)
available_qty = flt(qty) - flt(subcontracted_quantity)
if available_qty == 0:
continue
si.qty = available_qty
conversion_factor = po_item.qty / po_item.fg_item_qty
conversion_factor = flt(qty) / flt(fg_item_qty)
si.fg_item_qty = flt(
available_qty / conversion_factor, frappe.get_precision("Purchase Order Item", "qty")
)
@@ -338,13 +342,24 @@ class SubcontractingOrder(SubcontractingController):
def update_subcontracted_quantity_in_po(self, cancel=False):
for service_item in self.service_items:
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
doc.subcontracted_quantity = (
(doc.subcontracted_quantity + service_item.qty)
if not cancel
else (doc.subcontracted_quantity - service_item.qty)
subcontracted_quantity = flt(
frappe.db.get_value(
"Purchase Order Item", service_item.purchase_order_item, "subcontracted_quantity"
)
)
subcontracted_quantity = (
(subcontracted_quantity + service_item.qty)
if not cancel
else (subcontracted_quantity - service_item.qty)
)
frappe.db.set_value(
"Purchase Order Item",
service_item.purchase_order_item,
"subcontracted_quantity",
subcontracted_quantity,
)
doc.save()
@frappe.whitelist()

View File

@@ -336,6 +336,10 @@ frappe.ui.form.on("Subcontracting Receipt", {
reset_raw_materials_table: (frm) => {
frm.clear_table("supplied_items");
frm.doc.__unsaved = true;
if (!frm.doc.set_posting_time) {
frm.set_value("posting_time", frappe.datetime.now_time());
}
frm.call({
method: "reset_raw_materials",

View File

@@ -649,6 +649,7 @@
"label": "Raw Materials Actions"
},
{
"description": "Click this button if you encounter a negative stock error for a serial or batch item. The system will fetch the available serials or batches automatically.",
"fieldname": "reset_raw_materials_table",
"fieldtype": "Button",
"label": "Reset Raw Materials Table"
@@ -678,7 +679,7 @@
"in_create": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-12-06 15:24:38.384232",
"modified": "2025-10-08 21:43:27.065640",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt",
@@ -739,6 +740,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status, posting_date, supplier",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -116,7 +116,13 @@ class SubcontractingReceipt(SubcontractingController):
self.validate_items_qty()
self.set_items_bom()
self.set_items_cost_center()
self.set_items_expense_account()
if self.company:
default_expense_account = self.get_company_default(
"default_expense_account", ignore_validation=True
)
self.set_service_expense_account(default_expense_account)
self.set_expense_account_for_subcontracted_items(default_expense_account)
def validate(self):
self.reset_supplied_items()
@@ -196,6 +202,39 @@ class SubcontractingReceipt(SubcontractingController):
if item.subcontracting_order:
check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
def set_service_expense_account(self, default_expense_account):
for row in self.get("items"):
if not row.service_expense_account and row.purchase_order_item:
service_item = frappe.db.get_value(
"Purchase Order Item", row.purchase_order_item, "item_code"
)
if service_item:
if default := (
get_item_defaults(service_item, self.company)
or get_item_group_defaults(service_item, self.company)
or get_brand_defaults(service_item, self.company)
):
if service_expense_account := default.get("expense_account"):
row.service_expense_account = service_expense_account
if not row.service_expense_account:
row.service_expense_account = default_expense_account
def set_expense_account_for_subcontracted_items(self, default_expense_account):
for row in self.get("items"):
if not row.expense_account:
if default := (
get_item_defaults(row.item_code, self.company)
or get_item_group_defaults(row.item_code, self.company)
or get_brand_defaults(row.item_code, self.company)
):
if expense_account := default.get("expense_account"):
row.expense_account = expense_account
if not row.expense_account:
row.expense_account = default_expense_account
def validate_items_qty(self):
for item in self.items:
if not (item.qty or item.rejected_qty):
@@ -242,14 +281,6 @@ class SubcontractingReceipt(SubcontractingController):
self.company,
)
def set_items_expense_account(self):
if self.company:
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
for item in self.items:
if not item.expense_account:
item.expense_account = expense_account
def set_supplied_items_expense_account(self):
for item in self.supplied_items:
if not item.expense_account:
@@ -599,13 +630,17 @@ class SubcontractingReceipt(SubcontractingController):
project=item.project,
item=item,
)
service_cost = flt(
item.service_cost_per_qty, item.precision("service_cost_per_qty")
) * flt(item.qty, item.precision("qty"))
# Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=item.cost_center,
debit=0.0,
credit=stock_value_diff,
credit=flt(stock_value_diff) - service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
account_currency=get_account_currency(item.expense_account),
@@ -613,6 +648,21 @@ class SubcontractingReceipt(SubcontractingController):
item=item,
)
service_account = item.service_expense_account or item.expense_account
# Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=service_account,
cost_center=item.cost_center,
debit=0.0,
credit=service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
account_currency=get_account_currency(service_account),
project=item.project,
item=item,
)
if flt(item.rm_supp_cost) and supplier_warehouse_account:
for rm_item in supplied_items_details.get(item.name):
# Supplier Warehouse Account (Credit)

View File

@@ -422,6 +422,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_subcontracting_receipt_for_service_expense_account(self):
service_expense_account = (
frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Service Expense",
"account_type": "Expense Account",
"company": "_Test Company with perpetual inventory",
"is_group": 0,
"parent_account": "Indirect Expenses - TCP1",
}
)
.insert(ignore_if_duplicate=True)
.name
)
service_item_doc = frappe.get_doc("Item", "Subcontracted Service Item 10")
service_item_doc.append(
"item_defaults",
{
"company": "_Test Company with perpetual inventory",
"expense_account": service_expense_account,
"default_warehouse": "Stores - TCP1",
},
)
service_item_doc.save()
service_items = [
{
"warehouse": "Stores - TCP1",
"item_code": "Subcontracted Service Item 10",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA10",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
supplier_warehouse="Work In Progress - TCP1",
service_items=service_items,
)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr = make_subcontracting_receipt(sco.name)
scr.submit()
for item in scr.items:
self.assertEqual(item.service_expense_account, service_expense_account)
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
self.assertTrue(gl_entries)
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
expense_account = scr.items[0].expense_account
expected_values = {
fg_warehouse_ac: [2000, 1000],
expense_account: [1000, 1000],
service_expense_account: [0, 1000],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
@change_settings("Stock Settings", {"use_serial_batch_fields": 0})
def test_subcontracting_receipt_with_zero_service_cost(self):
warehouse = "Stores - TCP1"
@@ -740,13 +813,13 @@ class TestSubcontractingReceipt(FrappeTestCase):
for row in scr.supplied_items:
self.assertEqual(row.rate, 300.00)
self.assertTrue(row.serial_and_batch_bundle)
auto_created_serial_batch = frappe.db.get_value(
serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": scr.name, "voucher_detail_no": row.name},
"auto_created_serial_and_batch_bundle",
"serial_and_batch_bundle",
)
self.assertTrue(auto_created_serial_batch)
self.assertTrue(serial_and_batch_bundle)
self.assertEqual(scr.items[0].rm_cost_per_qty, 900)
self.assertEqual(scr.items[0].service_cost_per_qty, 100)

View File

@@ -64,6 +64,8 @@
"manufacturer_part_no",
"accounting_details_section",
"expense_account",
"column_break_exht",
"service_expense_account",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -580,12 +582,22 @@
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
},
{
"fieldname": "column_break_exht",
"fieldtype": "Column Break"
},
{
"fieldname": "service_expense_account",
"fieldtype": "Link",
"label": "Service Expense Account",
"options": "Account"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-06 15:23:58.680169",
"modified": "2025-09-26 12:00:38.877638",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",

View File

@@ -55,6 +55,7 @@ class SubcontractingReceiptItem(Document):
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
service_cost_per_qty: DF.Currency
service_expense_account: DF.Link | None
stock_uom: DF.Link
subcontracting_order: DF.Link | None
subcontracting_order_item: DF.Data | None

View File

@@ -233,6 +233,8 @@
"options": "Project"
},
{
"fetch_from": "email_account.company",
"fetch_if_empty": 1,
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -391,7 +393,7 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
"modified": "2025-02-18 21:18:52.797745",
"modified": "2025-09-25 11:10:53.556731",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",