Compare commits

..

117 Commits

Author SHA1 Message Date
Frappe PR Bot
34d3f58149 chore(release): Bumped to Version 14.85.1
## [14.85.1](https://github.com/frappe/erpnext/compare/v14.85.0...v14.85.1) (2025-04-10)

### Bug Fixes

* serial no validation for stock reconciliation ([c388b75](c388b7579c))

### Performance Improvements

* stock ageing report generation ([eaa2974](eaa297475f))
2025-04-10 06:02:52 +00:00
rohitwaghchaure
efd34817be Merge pull request #46986 from frappe/mergify/bp/version-14/pr-46971
fix: serial no validation for stock reconciliation (backport #46971)
2025-04-10 11:31:22 +05:30
rohitwaghchaure
124ad729ab Merge pull request #46984 from frappe/mergify/bp/version-14/pr-46983
perf: stock ageing report generation (backport #46983)
2025-04-10 11:31:13 +05:30
Rohit Waghchaure
c388b7579c fix: serial no validation for stock reconciliation
(cherry picked from commit a3d4d34454)
2025-04-10 05:36:45 +00:00
Rohit Waghchaure
eaa297475f perf: stock ageing report generation
(cherry picked from commit 7a74dac2c2)
2025-04-10 05:34:42 +00:00
Frappe PR Bot
24681bd64f chore(release): Bumped to Version 14.85.0
# [14.85.0](https://github.com/frappe/erpnext/compare/v14.84.0...v14.85.0) (2025-04-08)

### Bug Fixes

* **accounting:** update outstanding amount based on update_outstanding_for_self ([8e2bfc6](8e2bfc6bcb))
* don't filter payment entries on Bank Account in Payment Clearance ([576ce7e](576ce7e882))
* empty party filter on change of party type in General Ledger Report. ([14b4d14](14b4d147a8))
* improved rounding adjustment when applying discount (backport [#46720](https://github.com/frappe/erpnext/issues/46720)) ([06c3299](06c32993ea))
* incorrect condition ([d1f9444](d1f9444be7))
* make message translatable (backport [#46863](https://github.com/frappe/erpnext/issues/46863)) ([#46865](https://github.com/frappe/erpnext/issues/46865)) ([70d117e](70d117e858))
* **payment term:** allocate payment amount when payment term is fetched from order ([36b951d](36b951d018))
* remove against_voucher from General Ledger Report ([9e3a041](9e3a04136c))
* removed customer_group query in customer.js ([3be0f00](3be0f00b3b))
* Translate UnReconcile dialog title (backport [#46818](https://github.com/frappe/erpnext/issues/46818)) ([#46862](https://github.com/frappe/erpnext/issues/46862)) ([5f467be](5f467be0c8))
* update outstanding with precision ([698d5be](698d5be840))
* update payment amount if automatically_fetch_payment_terms is enabled ([dda35b8](dda35b8e51))
* update posting date before running validations ([430d1e8](430d1e8b2e))
* use `grand_total_diff` instead of `rounding_adjustment` in `taxes_and_totals` (backport [#46829](https://github.com/frappe/erpnext/issues/46829)) ([233e0d8](233e0d8049))
* user permissions in sales and purchase report ([f26b22e](f26b22ee7f))

### Features

* asset filter in asset depreciation and balances report ([#46848](https://github.com/frappe/erpnext/issues/46848)) ([2fe9fa7](2fe9fa7ef7))
2025-04-08 13:05:14 +00:00
ruthra kumar
b0c742b64e Merge pull request #46941 from frappe/version-14-hotfix
chore: release v14
2025-04-08 18:33:42 +05:30
mergify[bot]
18282b2632 chore: fix german translations (backport #46912) (#46914)
* chore: fix german translations (#46912)

(cherry picked from commit 8276e8e8b3)

# Conflicts:
#	erpnext/translations/de.csv

* chore: resolve conflicts

* chore: delete duplicate translations

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-04-08 13:16:35 +02:00
ruthra kumar
46115d6f71 Merge pull request #46937 from frappe/mergify/bp/version-14-hotfix/pr-46631
fix: update outstanding for self (backport #46631)
2025-04-08 16:21:45 +05:30
ruthra kumar
f62905f7a7 chore: pass individual range 2025-04-08 15:53:30 +05:30
ruthra kumar
88facb7523 refactor: pass both doctype and name 2025-04-08 15:13:59 +05:30
ruthra kumar
512877ab46 chore: resolve conflicts 2025-04-08 14:34:59 +05:30
Bhavan23
c7e6b2356f test: add unit test to validate outstanding amount for update_outstanding_for_self checkbox enabled
(cherry picked from commit 7b0882600a)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2025-04-08 08:59:05 +00:00
Bhavan23
8e2bfc6bcb fix(accounting): update outstanding amount based on update_outstanding_for_self
fix(accounting): against voucher has been already paid show proper message and update update_outstanding_for_self as 1

(cherry picked from commit 222f1834f1)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2025-04-08 08:59:04 +00:00
ruthra kumar
98260b990c Merge pull request #46931 from frappe/mergify/bp/version-14-hotfix/pr-46821
fix: removed customer_group query in customer.js (backport #46821)
2025-04-08 13:10:04 +05:30
ljain112
3be0f00b3b fix: removed customer_group query in customer.js
(cherry picked from commit f49adfdd98)
2025-04-08 07:28:33 +00:00
ruthra kumar
f44478db78 Merge pull request #46924 from frappe/mergify/bp/version-14-hotfix/pr-46709
fix: user permissions in sales and purchase report (backport #46709)
2025-04-08 11:53:08 +05:30
ruthra kumar
db8cb70bd8 chore: resolve conflict 2025-04-08 11:13:44 +05:30
ljain112
f26b22ee7f fix: user permissions in sales and purchase report
(cherry picked from commit f4bc1dfd00)

# Conflicts:
#	erpnext/accounts/report/purchase_register/purchase_register.py
2025-04-08 05:40:47 +00:00
ruthra kumar
1e4807f16d Merge pull request #46920 from frappe/mergify/bp/version-14-hotfix/pr-46804
fix: update outstanding with precision (backport #46804)
2025-04-08 11:07:45 +05:30
ljain112
698d5be840 fix: update outstanding with precision
(cherry picked from commit aadda9f606)
2025-04-08 04:29:31 +00:00
ruthra kumar
59e46e22d3 Merge pull request #46904 from frappe/mergify/bp/version-14-hotfix/pr-46895
fix: empty party filter on change of party type in General Ledger Report (backport #46895)
2025-04-07 17:55:56 +05:30
ljain112
14b4d147a8 fix: empty party filter on change of party type in General Ledger Report.
(cherry picked from commit 9c68bc22fa)
2025-04-07 12:12:55 +00:00
ruthra kumar
2bed1c8336 Merge pull request #46901 from frappe/mergify/bp/version-14-hotfix/pr-46900
fix: remove against_voucher and against_voucher_type column from General Ledger Report (backport #46900)
2025-04-07 17:38:42 +05:30
ljain112
9e3a04136c fix: remove against_voucher from General Ledger Report
(cherry picked from commit 6d1f119a0f)
2025-04-07 17:14:49 +05:30
ruthra kumar
dcc53cea55 Merge pull request #46896 from frappe/mergify/bp/version-14-hotfix/pr-46728
fix: update posting date before running validations (backport #46728)
2025-04-07 14:54:37 +05:30
Dany Robert
430d1e8b2e fix: update posting date before running validations
(cherry picked from commit d04dbd8ed9)
2025-04-07 09:01:56 +00:00
ruthra kumar
36a366d962 Merge pull request #46890 from frappe/mergify/bp/version-14-hotfix/pr-46637
fix(payment term): allocate payment amount when payment term is fetched from order (backport #46637)
2025-04-07 11:25:58 +05:30
venkat102
dda35b8e51 fix: update payment amount if automatically_fetch_payment_terms is enabled
(cherry picked from commit 7bf1a39861)
2025-04-07 05:34:19 +00:00
venkat102
4e0d7d88ec test: validate payment schedule based on invoice amount
(cherry picked from commit 7785296573)
2025-04-07 05:34:19 +00:00
venkat102
36b951d018 fix(payment term): allocate payment amount when payment term is fetched from order
(cherry picked from commit 5618859bd8)
2025-04-07 05:34:18 +00:00
mergify[bot]
233e0d8049 fix: use grand_total_diff instead of rounding_adjustment in taxes_and_totals (backport #46829)
* fix: use `grand_total_diff` instead of `rounding_adjustment` in `taxes_and_totals`

(cherry picked from commit fd252da6b1)

* test: ensure correct grand total

---------

Co-authored-by: vishakhdesai <vishakhdesai@gmail.com>
Co-authored-by: Sagar Vora <sagar@resilient.tech>
2025-04-06 09:11:06 +05:30
mergify[bot]
5f467be0c8 fix: Translate UnReconcile dialog title (backport #46818) (#46862)
fix: Translate UnReconcile dialog title

(cherry picked from commit f2cfb03c2c)

Co-authored-by: Corentin Forler <corentin@dokos.io>
2025-04-05 17:37:16 +02:00
mergify[bot]
70d117e858 fix: make message translatable (backport #46863) (#46865)
fix: make message translatable (#46863)

(cherry picked from commit 7d12e9afd4)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-04-05 17:36:16 +02:00
Khushi Rawat
2fe9fa7ef7 feat: asset filter in asset depreciation and balances report (#46848) 2025-04-04 10:39:16 +05:30
Sagar Vora
b4ca51ad4c Merge pull request #46813 from frappe/mergify/bp/version-14-hotfix/pr-46812
fix: revert resetting `rounding_adjustment` (backport #46812)
2025-03-31 15:56:27 +05:30
Vishakh Desai
fb5777cf10 Merge pull request #46812 from vishakhdesai/fix-taxes-and-totals
fix: revert resetting `rounding_adjustment`
(cherry picked from commit 3a9dca0563)
2025-03-31 10:25:00 +00:00
mergify[bot]
06c32993ea fix: improved rounding adjustment when applying discount (backport #46720)
* fix: improved rounding adjustment when applying discount (#46720)

* fix: rounding adjustment in apply_discount_amount taxes_and_totals

* refactor: minor changes

* fix: set the rounding difference while calculating tax total in the last tax row and add test case

* fix: failing test case

* fix: made changes in get_total_for_discount_amount in taxes_and_totals

* fix: failing test cases

* fix: changes as per review

* refactor: remove unnecessary use of flt

* refactor: improve logic

* refactor: minor change

* refactor: minor changes

* fix: add a test case for applying discount with previous row total in taxes

* fix: failing test case

* refactor: flatter code, remove `flt` usage for accuracy

---------

Co-authored-by: Vishakh Desai <78500008+vishakhdesai@users.noreply.github.com>
Co-authored-by: Sagar Vora <sagar@resilient.tech>
2025-03-31 15:54:09 +05:30
rohitwaghchaure
466c6ee3d4 Merge pull request #46780 from frappe/mergify/bp/version-14-hotfix/pr-46777
fix: incorrect condition (backport #46777)
2025-03-29 12:17:59 +05:30
rohitwaghchaure
a8831351e3 chore: fix conflicts 2025-03-29 12:16:17 +05:30
Rohit Waghchaure
d1f9444be7 fix: incorrect condition
(cherry picked from commit 0c1a8e9c58)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2025-03-28 18:19:41 +00:00
rohitwaghchaure
ceaea16d84 Merge pull request #46751 from frappe/mergify/bp/version-14/pr-46748
Revert "perf: timeout while renaming cost center (backport #46641)" (backport #46748)
2025-03-27 13:13:59 +05:30
rohitwaghchaure
119f2e7074 Revert "perf: timeout while renaming cost center (backport #46641)"
(cherry picked from commit da6affbba7)
2025-03-27 06:53:19 +00:00
rohitwaghchaure
9cadb89678 Merge pull request #46748 from frappe/revert-46646-mergify/bp/version-14-hotfix/pr-46641
Revert "perf: timeout while renaming cost center (backport #46641)"
2025-03-27 12:16:56 +05:30
rohitwaghchaure
da6affbba7 Revert "perf: timeout while renaming cost center (backport #46641)" 2025-03-27 11:57:24 +05:30
Sagar Vora
6293b08540 Merge pull request #46729 from vishakhdesai/bank-clearance-fix-v14-backport
fix: don't filter payment entries on Bank Account in Payment Clearance (backport #46669)
2025-03-26 13:00:57 +05:30
vishakhdesai
576ce7e882 fix: don't filter payment entries on Bank Account in Payment Clearance 2025-03-26 11:41:35 +05:30
Frappe PR Bot
67d7ffa430 chore(release): Bumped to Version 14.84.0
# [14.84.0](https://github.com/frappe/erpnext/compare/v14.83.4...v14.84.0) (2025-03-25)

### Bug Fixes

* assign dialog instance to a variable in update_child_items function ([690a939](690a939572))
* customer credit limit check based on `bypass_credit_limit_check` in Journal Entry ([331ecc1](331ecc1964))
* do not validate if conversion rate is 1 for different currency ([c3447c0](c3447c030a))
* fix lint issues with trailing whitespaces ([df64d2e](df64d2ef4e))
* removed test case ([509c5c4](509c5c4d17))

### Features

* repost accounting ledger for purchase receipt ([88e664b](88e664b79f))

### Performance Improvements

* timeout while renaming cost center ([b9b08c3](b9b08c35ef))
2025-03-25 12:25:57 +00:00
ruthra kumar
d90c8a9150 Merge pull request #46714 from frappe/version-14-hotfix
chore: release v14
2025-03-25 17:54:27 +05:30
ruthra kumar
15bb1fdb24 Merge pull request #46712 from frappe/mergify/bp/version-14-hotfix/pr-46616
fix: do not validate if conversion rate is 1 for different currency (backport #46616)
2025-03-25 16:06:08 +05:30
ljain112
509c5c4d17 fix: removed test case 2025-03-25 15:22:19 +05:30
ruthra kumar
cb028b8740 chore: resolve conflict 2025-03-25 14:26:14 +05:30
ljain112
c3447c030a fix: do not validate if conversion rate is 1 for different currency
(cherry picked from commit e8a66d03bc)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2025-03-25 08:42:17 +00:00
ruthra kumar
1d65f995e7 Merge pull request #46646 from frappe/mergify/bp/version-14-hotfix/pr-46641
perf: timeout while renaming cost center (backport #46641)
2025-03-25 13:27:09 +05:30
ruthra kumar
3a6d8cd9a1 Merge pull request #46704 from frappe/mergify/bp/version-14-hotfix/pr-46622
feat: repost accounting ledger for purchase receipt (backport #46622)
2025-03-25 12:13:50 +05:30
ljain112
88e664b79f feat: repost accounting ledger for purchase receipt
(cherry picked from commit b36e356469)
2025-03-25 06:19:33 +00:00
ruthra kumar
090ee7f042 Merge pull request #46690 from IMS94/fix-46686-so-update-items
fix: assign dialog instance to a variable in update_child_items function
2025-03-25 11:18:45 +05:30
mergify[bot]
b311b6eb7f ci: apply label "skip-release-notes" based on PR title (backport #46694) (#46696)
ci: apply label "skip-release-notes" based on PR title (#46694)

Workflow copied from frappe/frappe

(cherry picked from commit eb350012b0)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-03-24 16:55:07 +01:00
Imesha Sudasingha
df64d2ef4e fix: fix lint issues with trailing whitespaces 2025-03-24 12:50:48 +00:00
Imesha Sudasingha
690a939572 fix: assign dialog instance to a variable in update_child_items function
`dialog` is being referred in `onchange` handlers of the fields. Without this fix, they are failing because `dialog` is not defined.
2025-03-24 12:42:42 +00:00
ruthra kumar
a3639b055c Merge pull request #46676 from frappe/mergify/bp/version-14-hotfix/pr-46577
fix: customer credit limit check based on `bypass_credit_limit_check` in Journal Entry (backport #46577)
2025-03-24 13:58:00 +05:30
ljain112
331ecc1964 fix: customer credit limit check based on bypass_credit_limit_check in Journal Entry
(cherry picked from commit 8a84faebed)
2025-03-24 08:01:38 +00:00
rohitwaghchaure
736b125e14 chore: fix conflicts 2025-03-22 20:49:56 +05:30
Rohit Waghchaure
b9b08c35ef perf: timeout while renaming cost center
(cherry picked from commit 92be7cbbbf)

# Conflicts:
#	erpnext/accounts/doctype/gl_entry/gl_entry.json
2025-03-21 12:38:52 +00:00
Frappe PR Bot
7e8d2dc0fc chore(release): Bumped to Version 14.83.4
## [14.83.4](https://github.com/frappe/erpnext/compare/v14.83.3...v14.83.4) (2025-03-19)

### Bug Fixes

* fetch quality inspection parameter group ([6e497f7](6e497f73f1))
* not able to make PR against stand alone Debit Note ([16fe53b](16fe53b8c7))
* order by condition ([af3089b](af3089b96e))
* repost future sle and gle after capitalization ([#46576](https://github.com/frappe/erpnext/issues/46576)) ([805549f](805549f793))
* set stock adjustment account in difference account ([6f0c67a](6f0c67a242))
* **Transaction Deletion Record:** sql syntax error while fetching lead address ([20b43b4](20b43b4d93))
2025-03-19 11:04:55 +00:00
ruthra kumar
39af6bd143 Merge pull request #46581 from frappe/version-14-hotfix
chore: release v14
2025-03-19 16:33:27 +05:30
rohitwaghchaure
300f1a860b Merge pull request #46609 from rohitwaghchaure/fixed-support-33137
fix: order by condition
2025-03-19 15:36:43 +05:30
rohitwaghchaure
1441ef2532 Merge pull request #46610 from frappe/mergify/bp/version-14-hotfix/pr-46595
fix: not able to make PR against stand alone Debit Note (backport #46595)
2025-03-19 15:36:15 +05:30
rohitwaghchaure
64fd5c0ba9 chore: fix conflicts 2025-03-19 15:31:05 +05:30
Rohit Waghchaure
16fe53b8c7 fix: not able to make PR against stand alone Debit Note
(cherry picked from commit 6a52c30591)

# Conflicts:
#	erpnext/public/js/controllers/buying.js
2025-03-19 08:05:24 +00:00
Rohit Waghchaure
af3089b96e fix: order by condition 2025-03-19 13:20:56 +05:30
rohitwaghchaure
3cb29fc035 Merge pull request #46583 from frappe/mergify/bp/version-14-hotfix/pr-46575
fix: fetch quality inspection parameter group (backport #46575)
2025-03-18 17:28:54 +05:30
rohitwaghchaure
8a93057844 Merge pull request #46586 from frappe/mergify/bp/version-14-hotfix/pr-46576
fix: repost future sle and gle after capitalization (backport #46576)
2025-03-18 17:28:15 +05:30
Khushi Rawat
805549f793 fix: repost future sle and gle after capitalization (#46576)
(cherry picked from commit 29d77aa19f)
2025-03-18 10:31:48 +00:00
Mihir Kandoi
6e497f73f1 fix: fetch quality inspection parameter group
(cherry picked from commit 0a482c7ea8)
2025-03-18 10:27:29 +00:00
ruthra kumar
33f090a8c8 Merge pull request #46561 from frappe/mergify/bp/version-14-hotfix/pr-46557
fix(Transaction Deletion Record): sql syntax error while deleting lead address (backport #46557)
2025-03-17 17:00:49 +05:30
venkat102
20b43b4d93 fix(Transaction Deletion Record): sql syntax error while fetching lead address
(cherry picked from commit af0d6eeae8)
2025-03-17 11:08:38 +00:00
ruthra kumar
5c1f6f0107 Merge pull request #46511 from aerele/fix-v14/item-stock-difference-account
fix: set stock adjustment account in difference account (backport #45606)
2025-03-14 09:23:52 +05:30
Bhavan23
6f0c67a242 fix: set stock adjustment account in difference account 2025-03-13 17:01:55 +05:30
Frappe PR Bot
609bfaefba chore(release): Bumped to Version 14.83.3
## [14.83.3](https://github.com/frappe/erpnext/compare/v14.83.2...v14.83.3) (2025-03-12)

### Bug Fixes

* **account:** update account number from parent company ([1f54c27](1f54c272ac))
* not able to save work order with alternative item ([6ca1f9b](6ca1f9bc73))
2025-03-12 14:13:24 +00:00
ruthra kumar
2546589b4c Merge pull request #46445 from frappe/version-14-hotfix
chore: release v14
2025-03-12 19:41:47 +05:30
Shanuka Hewage
f311a0fc1c Fix: add parenttype condition to payment schedule query in accounts receivable report (#46370)
fix: add parenttype condition to payment schedule query in accounts receivable report
2025-03-12 16:07:07 +05:30
mergify[bot]
a1585b3c53 ci: ignore PRs labeled with "skip-release-notes" when generating release notes (backport #46453) (#46456)
ci: ignore PRs labeled with "skip-release-notes" when generating release notes

(cherry picked from commit 57007bf937)

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-03-12 10:33:07 +05:30
ruthra kumar
05c0bf5d99 Merge pull request #46449 from frappe/mergify/bp/version-14-hotfix/pr-46398
chore: rename print and stationery account (backport #46398)
2025-03-11 18:12:08 +05:30
chethank1407
247a006b5a chore: rename print and stationery account
(cherry picked from commit 615997b774)
2025-03-11 12:03:21 +00:00
ruthra kumar
e0cf6393ee Merge pull request #46438 from frappe/mergify/bp/version-14-hotfix/pr-46372
fix(account): update account number from parent company (backport #46372)
2025-03-11 11:24:35 +05:30
venkat102
1f54c272ac fix(account): update account number from parent company
(cherry picked from commit 4a4894bc01)
2025-03-11 05:32:05 +00:00
rohitwaghchaure
9a1321ab02 Merge pull request #46414 from rohitwaghchaure/fixed-support-32397
fix: not able to save work order with alternative item
2025-03-10 17:38:37 +05:30
Rohit Waghchaure
6ca1f9bc73 fix: not able to save work order with alternative item 2025-03-10 12:46:45 +05:30
Frappe PR Bot
b404f00ece chore(release): Bumped to Version 14.83.2
## [14.83.2](https://github.com/frappe/erpnext/compare/v14.83.1...v14.83.2) (2025-03-05)

### Bug Fixes

* Add permission check in POS's `Toggle Recent Orders` (backport [#46010](https://github.com/frappe/erpnext/issues/46010)) ([#46273](https://github.com/frappe/erpnext/issues/46273)) ([557a05b](557a05b0ad))
* auto allocation for negative amount outstanding for Customers in Payment Entry ([8e02dcf](8e02dcfcaa))
* change voucher_type and voucher_no field type to data ([df4f4d9](df4f4d9a31))
* consolidate gl entries by project in General Ledger Report (backport [#46314](https://github.com/frappe/erpnext/issues/46314)) ([#46320](https://github.com/frappe/erpnext/issues/46320)) ([b3b7e62](b3b7e62a90))
* do not include opening invoices in billed items to be received report ([cedf577](cedf577b4c))
* don't allow renaming account while system is actively in use (backport [#46176](https://github.com/frappe/erpnext/issues/46176)) ([#46209](https://github.com/frappe/erpnext/issues/46209)) ([167069b](167069b823))
* exclude cancelled gl entries ([369a692](369a692af9))
* incorrect batch picked in the pick list ([ac25d3e](ac25d3e1c4))
* incorrectly billed amount in the purchase receipt ([ca94ad3](ca94ad3a24))
* inventory dimension for maintence visit ([69a8e0d](69a8e0dfac))
* only include submitted docs for internal received quantity validation (backport [#46262](https://github.com/frappe/erpnext/issues/46262)) ([#46303](https://github.com/frappe/erpnext/issues/46303)) ([90b5f0b](90b5f0b7bf))
* **patch:** Ensure SLE indexes (backport [#46131](https://github.com/frappe/erpnext/issues/46131)) ([#46134](https://github.com/frappe/erpnext/issues/46134)) ([f8bbcab](f8bbcab3a5))

### Performance Improvements

* don't track seen for POS Invoice (backport [#46187](https://github.com/frappe/erpnext/issues/46187)) ([#46188](https://github.com/frappe/erpnext/issues/46188)) ([b41ee66](b41ee667b9))
2025-03-05 13:28:51 +00:00
ruthra kumar
0f56fbcf6d Merge pull request #46264 from frappe/version-14-hotfix
chore: release v14
2025-03-05 18:57:30 +05:30
mergify[bot]
b3b7e62a90 fix: consolidate gl entries by project in General Ledger Report (backport #46314) (#46320)
fix: consolidate gl entries by project in General Ledger Report (#46314)

(cherry picked from commit 1f685efcaf)

Co-authored-by: Lakshit Jain <108322669+ljain112@users.noreply.github.com>
2025-03-05 16:18:34 +05:30
rohitwaghchaure
35a728d22b Merge pull request #46316 from rohitwaghchaure/fixed-support-31024-v14
fix: incorrect batch picked in the pick list
2025-03-05 15:43:04 +05:30
Rohit Waghchaure
ac25d3e1c4 fix: incorrect batch picked in the pick list 2025-03-05 14:29:22 +05:30
mergify[bot]
90b5f0b7bf fix: only include submitted docs for internal received quantity validation (backport #46262) (#46303)
fix: only include submitted docs for internal received quantity validation (#46262)

(cherry picked from commit 88fcdbb81e)

Co-authored-by: Lakshit Jain <108322669+ljain112@users.noreply.github.com>
2025-03-05 13:17:17 +05:30
mergify[bot]
557a05b0ad fix: Add permission check in POS's Toggle Recent Orders (backport #46010) (#46273)
* fix: use get_list to check permissions

(cherry picked from commit a08bc6b913)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py

* fix: resolve conflicts

---------

Co-authored-by: Sanket322 <shahsanket322003.com>
Co-authored-by: ljain112 <ljain112@gmail.com>
2025-03-05 12:04:39 +05:30
mergify[bot]
b41ee667b9 perf: don't track seen for POS Invoice (backport #46187) (#46188)
* perf: don't track seen for POS Invoice (#46187)

This is a moving doctype. Do people even browse the list view?

It doesn't make much sense, either. POS INvoices are rarely "reviewed" by multiple users.

(cherry picked from commit ded0aab680)

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

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-03-05 12:03:27 +05:30
mergify[bot]
fbf6d8c9e2 chore: erpnext.com -> frappe.io/erpnext (backport #46288) (#46289)
* chore: erpnext.com -> frappe.io/erpnext (#46288)

(cherry picked from commit 41fe30ea6e)

# Conflicts:
#	README.md
#	pyproject.toml

* Update README.md

* Update pyproject.toml

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-03-05 11:37:49 +05:30
mergify[bot]
167069b823 fix: don't allow renaming account while system is actively in use (backport #46176) (#46209)
* fix: don't allow renaming account while system is actively in use (#46176)

(cherry picked from commit 999f1cf96d)

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

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-03-05 10:37:56 +05:30
Lakshit Jain
2d72a37d0c Merge pull request #46275 from frappe/mergify/bp/version-14-hotfix/pr-46039
fix: Ensure new line is added regardless of postal code presence (backport #46039)
2025-03-04 19:14:28 +05:30
Lakshit Jain
77ad224cf6 Merge pull request #46267 from frappe/mergify/bp/version-14-hotfix/pr-45896
fix: auto allocation for negative amount outstanding for Customers in Payment Entry (backport #45896)
2025-03-04 18:17:28 +05:30
Lakshit Jain
b5b55fad6c Merge pull request #46269 from frappe/mergify/bp/version-14-hotfix/pr-46260
fix: do not include opening invoices in billed items to be received report (backport #46260)
2025-03-04 18:16:57 +05:30
Sanket322
7cde990d69 refactor: add new line ragardless of postal code
(cherry picked from commit 746adfd057)
2025-03-04 12:36:40 +00:00
ljain112
cedf577b4c fix: do not include opening invoices in billed items to be received report
(cherry picked from commit c1ddf444c6)
2025-03-04 11:24:08 +00:00
ljain112
8e02dcfcaa fix: auto allocation for negative amount outstanding for Customers in Payment Entry
(cherry picked from commit 6275b44a0b)
2025-03-04 11:20:12 +00:00
Smit Vora
399ed331e3 Merge pull request #46247 from frappe/mergify/bp/version-14-hotfix/pr-46192
fix: Exclude Cancelled GL Entries (backport #46192)
2025-03-04 16:39:47 +05:30
ruthra kumar
59236bc5bf Merge pull request #46265 from frappe/mergify/bp/version-14-hotfix/pr-45751
fix: change voucher_type and voucher_name field type to data (backport #45751)
2025-03-04 16:12:16 +05:30
ruthra kumar
1089cdf213 chore: resolve conflicts 2025-03-04 15:46:05 +05:30
Sugesh393
df4f4d9a31 fix: change voucher_type and voucher_no field type to data
(cherry picked from commit f8ab021920)

# Conflicts:
#	erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
#	erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
2025-03-04 10:10:04 +00:00
mergify[bot]
f8bbcab3a5 fix(patch): Ensure SLE indexes (backport #46131) (#46134)
* fix(patch): Ensure SLE indexes (#46131)

Because of the way this change was pushed in parts, some sites don't see
this as "update" and don't have the new indexes.

(cherry picked from commit f62aa8fc57)

# Conflicts:
#	erpnext/patches.txt

* fix: resolved conflict

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2025-03-03 21:36:57 +05:30
Ninad1306
369a692af9 fix: exclude cancelled gl entries
(cherry picked from commit 3251a331dd)
2025-03-03 11:31:08 +00:00
rohitwaghchaure
8cc92e9ca0 Merge pull request #46241 from frappe/mergify/bp/version-14-hotfix/pr-46239
fix: incorrectly billed amount in the purchase receipt (backport #46239)
2025-03-03 16:16:38 +05:30
rohitwaghchaure
eebf6cf877 chore: fix conflicts 2025-03-03 14:48:07 +05:30
Rohit Waghchaure
ca94ad3a24 fix: incorrectly billed amount in the purchase receipt
(cherry picked from commit a5271fdb2e)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2025-03-03 09:15:48 +00:00
rohitwaghchaure
1a8bf0cf3d Merge pull request #46040 from frappe/mergify/bp/version-14-hotfix/pr-46037
fix: inventory dimension for maintenance visit (backport #46037)
2025-02-28 14:53:56 +05:30
rohitwaghchaure
811953b9b0 chore: fix conflicts 2025-02-23 19:47:06 +05:30
Rohit Waghchaure
69a8e0dfac fix: inventory dimension for maintence visit
(cherry picked from commit cd4ba69262)

# Conflicts:
#	erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
2025-02-20 06:13:14 +00:00
60 changed files with 3678 additions and 2195 deletions

4
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
changelog:
exclude:
labels:
- skip-release-notes

View File

@@ -0,0 +1,30 @@
name: "Auto-label PRs based on title"
on:
pull_request_target:
types: [opened, reopened]
jobs:
add-label-if-prefix-matches:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Check PR title and add label if it matches prefixes
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const title = context.payload.pull_request.title.toLowerCase();
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
// Check if the PR title starts with any of the prefixes
if (prefixes.some(prefix => title.startsWith(prefix))) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['skip-release-notes']
});
}

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.83.1"
__version__ = "14.85.1"
def get_default_company(user=None):

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, throw
from frappe.utils import cint, cstr
from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account:
return
@@ -420,7 +421,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
"name",
)
if old_name:
if old_name and not from_descendant:
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -514,3 +516,27 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)
def _ensure_idle_system():
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
if frappe.flags.in_test:
return
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
if last_gl_update > add_to_date(None, minutes=-5):
frappe.throw(
_(
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)

View File

@@ -98,7 +98,7 @@
"Office Maintenance Expenses": {},
"Office Rent": {},
"Postal Expenses": {},
"Print and Stationary": {},
"Print and Stationery": {},
"Rounded Off": {
"account_type": "Round Off"
},

View File

@@ -46,9 +46,6 @@ class BankClearance(Document):
as_dict=1,
)
if self.bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
f"""
select
@@ -70,7 +67,6 @@ class BankClearance(Document):
"account": self.account,
"from": self.from_date,
"to": self.to_date,
"bank_account": self.bank_account,
},
as_dict=1,
)
@@ -93,7 +89,7 @@ class BankClearance(Document):
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.where(loan_disbursement.disbursement_account == self.account)
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
@@ -121,7 +117,7 @@ class BankClearance(Document):
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
.where(loan_repayment.payment_account == self.account)
)
if not self.include_reconciled_entries:

View File

@@ -433,8 +433,22 @@ class JournalEntry(AccountsController):
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
customer_details = frappe._dict(
frappe.db.get_all(
"Customer Credit Limit",
filters={
"parent": ["in", customers],
"parenttype": ["=", "Customer"],
"company": ["=", self.company],
},
fields=["parent", "bypass_credit_limit_check"],
as_list=True,
)
)
for customer in customers:
check_credit_limit(customer, self.company)
ignore_outstanding_sales_order = bool(customer_details.get(customer))
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
def validate_cheque_info(self):
if self.voucher_type in ["Bank Entry"]:

View File

@@ -1519,7 +1519,7 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"):
elif self.party_type in ("Supplier", "Customer"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(

View File

@@ -1623,6 +1623,5 @@
"states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
"track_changes": 1
}

View File

@@ -1852,7 +1852,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
1,
)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
@@ -1984,6 +1984,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertRaises(frappe.ValidationError, dr_note.save)
def test_apply_discount_on_grand_total(self):
"""
To test if after applying discount on grand total,
the grand total is calculated correctly without any rounding errors
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 21.39,
},
)
invoice.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"rate": 15.5,
},
)
# the grand total here will be 255.71
invoice.disable_rounded_total = 1
# apply discount on grand total to adjust the grand total to 255
invoice.discount_amount = 0.71
invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
"""
To test if after applying discount on grand total,
where the tax is calculated on previous row total, the grand total is calculated correctly
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice.extend(
"taxes",
[
{
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"tax_amount": 100,
},
{
"charge_type": "On Previous Row Amount",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"row_id": 1,
"rate": 10,
},
{
"charge_type": "On Previous Row Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"row_id": 1,
"rate": 10,
},
],
)
# the total here will be 340, so applying 40 discount
invoice.discount_amount = 40
invoice.save()
self.assertEqual(invoice.grand_total, 300)
def check_gl_entries(
doc,

View File

@@ -6,6 +6,8 @@ from frappe import _, qb
from frappe.model.document import Document
from frappe.utils.data import comma_and
from erpnext.stock import get_warehouse_account_map
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
@@ -77,6 +79,9 @@ class RepostAccountingLedger(Document):
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
elif doc.doctype == "Purchase Receipt":
warehouse_account_map = get_warehouse_account_map(doc.company)
gle_map = doc.get_gl_entries(warehouse_account_map)
else:
gle_map = doc.get_gl_entries()
@@ -155,6 +160,14 @@ def start_repost(account_repost_doc=str) -> None:
doc.force_set_against_expense_account()
doc.make_gl_entries()
elif doc.doctype == "Purchase Receipt":
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
doc.make_gl_entries(from_repost=True)
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)

View File

@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
@@ -202,9 +204,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_06_repost_purchase_receipt(self):
from erpnext.accounts.doctype.account.test_account import create_account
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
another_provisional_account = create_account(
account_name="Another Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
company = frappe.get_doc("Company", self.company)
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
test_cc = company.cost_center
default_expense_account = company.default_expense_account
item = make_item(properties={"is_stock_item": 0})
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles = [
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
]
self.assertEqual(expected_pr_gles, pr_gl_entries)
# change the provisional account
frappe.db.set_value(
"Purchase Receipt Item",
pr.items[0].name,
"provisional_expense_account",
another_provisional_account,
)
repost_doc = frappe.new_doc("Repost Accounting Ledger")
repost_doc.company = self.company
repost_doc.delete_cancelled_entries = True
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
repost_doc.save().submit()
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles_after_repost = [
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
]
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
# teardown
repost_doc.cancel()
repost_doc.delete()
pr.reload()
pr.cancel()
company.enable_provisional_accounting_for_non_stock_items = 0
company.default_provisional_account = None
company.save()
def update_repost_settings():
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
allowed_types = [
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Purchase Receipt",
]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})

View File

@@ -91,8 +91,8 @@ class SalesInvoice(SellingController):
self.indicator_title = _("Paid")
def validate(self):
super().validate()
self.validate_auto_set_posting_time()
super().validate()
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()

View File

@@ -416,9 +416,9 @@ class TestSalesInvoice(FrappeTestCase):
for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.base_grand_total, 1500.01)
self.assertEqual(si.grand_total, 1500.01)
self.assertEqual(si.rounding_adjustment, -0.01)
self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.rounding_adjustment, 0)
def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@@ -1773,17 +1773,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(gle)
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self):
# Customer currency = USD
@@ -3871,6 +3860,35 @@ class TestSalesInvoice(FrappeTestCase):
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
def test_create_return_invoice_for_self_update(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.controllers.sales_and_purchase_return import make_return_doc
invoice = create_sales_invoice()
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
payment_entry.reference_no = "test001"
payment_entry.reference_date = getdate()
payment_entry.save()
payment_entry.submit()
r_invoice = make_return_doc(invoice.doctype, invoice.name)
r_invoice.update_outstanding_for_self = 0
r_invoice.save()
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
r_invoice.submit()
self.assertNotEqual(r_invoice.outstanding_amount, 0)
invoice.reload()
self.assertEqual(invoice.outstanding_amount, 0)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -13,17 +13,15 @@
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Type",
"options": "DocType"
"label": "Voucher Type"
},
{
"fieldname": "voucher_name",
"fieldtype": "Dynamic Link",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Name",
"options": "voucher_type"
"label": "Voucher Name"
},
{
"fieldname": "taxable_amount",
@@ -36,7 +34,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-01-13 13:40:41.479208",
"modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",

View File

@@ -498,7 +498,7 @@ class ReceivablePayableReport:
ps.description, ps.paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date

View File

@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
rate=100,
price_list_rate=100,
do_not_save=1,
**args,
)
if not no_payment_schedule:
si.append(
@@ -111,7 +112,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
def test_accounts_receivable(self):
def test_accounts_receivable_with_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
@@ -151,11 +152,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
self.assertEqual(cr_note.update_outstanding_for_self, True)
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
row = report[1][0]
row = report[1][-1]
self.assertEqual(
expected_data_after_credit_note,
[
@@ -168,6 +173,105 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
],
)
def test_accounts_receivable_without_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
si = self.create_sales_invoice()
report = execute(filters)
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
self.assertEqual(cr_note.update_outstanding_for_self, False)
report = execute(filters)
row = report[1]
self.assertTrue(len(row) == 0)
def test_accounts_receivable_with_partial_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
si = self.create_sales_invoice(qty=2)
report = execute(filters)
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(
expected_data_after_payment[i - 1],
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
self.assertFalse(cr_note.update_outstanding_for_self)
report = execute(filters)
expected_data_after_credit_note = [
[200, 100, 0, 80, 20, self.debit_to],
[200, 40, 0, 0, 40, self.debit_to],
]
for i in range(2):
row = report[1][i - 1]
self.assertEqual(
expected_data_after_credit_note[i - 1],
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.party_account,
],
)
def test_cr_note_flag_to_update_self(self):
filters = {
"company": self.company,

View File

@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
reqd: 1,
},
{
fieldname: "group_by",
label: __("Group By"),
fieldtype: "Select",
options: ["Asset Category", "Asset"],
default: "Asset Category",
},
{
fieldname: "asset_category",
label: __("Asset Category"),
fieldtype: "Link",
options: "Asset Category",
depends_on: "eval: doc.group_by == 'Asset Category'",
},
{
fieldname: "asset",
label: __("Asset"),
fieldtype: "Link",
options: "Asset",
depends_on: "eval: doc.group_by == 'Asset'",
},
],
};

View File

@@ -14,21 +14,28 @@ def execute(filters=None):
def get_data(filters):
if filters.get("group_by") == "Asset Category":
return get_group_by_asset_category_data(filters)
elif filters.get("group_by") == "Asset":
return get_group_by_asset_data(filters)
def get_group_by_asset_category_data(filters):
data = []
asset_categories = get_asset_categories(filters)
assets = get_assets(filters)
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
# row.asset_category = asset_category
row.update(asset_category)
row.cost_as_on_to_date = (
flt(row.cost_as_on_from_date)
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
)
row.update(
@@ -38,17 +45,19 @@ def get_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "")
)
)
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
@@ -57,52 +66,71 @@ def get_data(filters):
return data
def get_asset_categories(filters):
def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
condition += " and a.asset_category = %(asset_category)s"
# nosemgrep
return frappe.db.sql(
f"""
SELECT asset_category,
ifnull(sum(case when purchase_date < %(from_date)s then
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
SELECT a.asset_category,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Sold" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Scrapped" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
group by asset_category
end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.asset_category
""",
{
"to_date": filters.to_date,
@@ -114,14 +142,17 @@ def get_asset_categories(filters):
)
def get_assets(filters):
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
# nosemgrep
return frappe.db.sql(
"""
f"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
@@ -130,6 +161,11 @@ def get_assets(filters):
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
@@ -149,15 +185,22 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition}
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
@@ -165,51 +208,272 @@ def get_assets(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.asset_category) as results
group by results.asset_category
""".format(condition),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
},
as_dict=1,
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
condition += " and a.name = %(asset)s"
# nosemgrep
return frappe.db.sql(
f"""
SELECT a.name,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Sold" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Scrapped" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.name
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset": filters.get("asset"),
},
as_dict=1,
)
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
condition = f" and a.name = '{filters.get('asset')}'"
# nosemgrep
return frappe.db.sql(
f"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition}
group by a.name
union
SELECT a.name as name,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_as_on_from_date_credit,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.name) as results
group by results.name
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
},
as_dict=1,
)
def get_columns(filters):
return [
columns = []
if filters.get("group_by") == "Asset Category":
columns.append(
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
}
)
elif filters.get("group_by") == "Asset":
columns.append(
{
"label": _("Asset"),
"fieldname": "asset",
"fieldtype": "Link",
"options": "Asset",
"width": 120,
}
)
columns += [
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
},
{
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date",
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "value_as_on_from_date",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost of New Purchase"),
"fieldname": "cost_of_new_purchase",
"label": _("Value of New Purchase"),
"fieldname": "value_of_new_purchase",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost of Sold Asset"),
"fieldname": "cost_of_sold_asset",
"label": _("Value of Sold Asset"),
"fieldname": "value_of_sold_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost of Scrapped Asset"),
"fieldname": "cost_of_scrapped_asset",
"label": _("Value of Scrapped Asset"),
"fieldname": "value_of_scrapped_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost as on") + " " + formatdate(filters.to_date),
"fieldname": "cost_as_on_to_date",
"label": _("Value of New Capitalized Asset"),
"fieldname": "value_of_capitalized_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value as on") + " " + formatdate(filters.to_date),
"fieldname": "value_as_on_to_date",
"fieldtype": "Currency",
"width": 140,
},
@@ -237,6 +501,12 @@ def get_columns(filters):
"fieldtype": "Currency",
"width": 270,
},
{
"label": _("Depreciation eliminated via reversal"),
"fieldname": "depreciation_eliminated_via_reversal",
"fieldtype": "Currency",
"width": 270,
},
{
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "net_asset_value_as_on_from_date",
@@ -250,3 +520,5 @@ def get_columns(filters):
"width": 200,
},
]
return columns

View File

@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0],
["Purchase Invoice", "is_opening", "!=", "Yes"],
]
if report_filters.get("purchase_invoice"):

View File

@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
and ba.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select

View File

@@ -61,7 +61,7 @@ frappe.query_reports["General Ledger"] = {
fieldtype: "Autocomplete",
options: Object.keys(frappe.boot.party_account_types),
on_change: function () {
frappe.query_report.set_filter_value("party", "");
frappe.query_report.set_filter_value("party", []);
},
},
{

View File

@@ -468,9 +468,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key][rev_dr_or_cr] = 0
data[key][rev_dr_or_cr + "_in_account_currency"] = 0
if data[key].against_voucher and gle.against_voucher:
data[key].against_voucher += ", " + gle.against_voucher
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
show_opening_entries = filters.get("show_opening_entries")
@@ -506,6 +503,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
keylist.append(gle.get("project"))
key = tuple(keylist)
if key not in consolidated_gle:
@@ -617,10 +615,11 @@ def get_columns(filters):
{"label": _("Against Account"), "fieldname": "against", "width": 120},
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
{"label": _("Party"), "fieldname": "party", "width": 100},
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
]
if filters.get("include_dimensions"):
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
for dim in get_accounting_dimensions(as_list=False):
columns.append(
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
@@ -631,14 +630,6 @@ def get_columns(filters):
columns.extend(
[
{"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100},
{
"label": _("Against Voucher"),
"fieldname": "against_voucher",
"fieldtype": "Dynamic Link",
"options": "against_voucher_type",
"width": 100,
},
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
]
)

View File

@@ -11,7 +11,7 @@ import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
add_sub_total_row,
add_total_row,
apply_group_by_conditions,
apply_order_by_conditions,
get_grand_total,
get_group_by_and_display_fields,
get_tax_accounts,
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
if filters.get("item_group"):
query = query.where(pii.item_group == filters.get("item_group"))
if not filters.get("group_by"):
query = query.orderby(pi.posting_date, order=Order.desc)
query = query.orderby(pii.item_group, order=Order.desc)
else:
query = apply_group_by_conditions(query, pi, pii, filters)
return query
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
query = apply_conditions(query, pi, pii, filters)
return query.run(as_dict=True)
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice")
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, pi, pii, filters)
return frappe.db.sql(query, params, as_dict=True)
def get_aii_accounts():

View File

@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
| (si.unrealized_profit_loss_account == filters.get("income_account"))
)
if not filters.get("group_by"):
query = query.orderby(si.posting_date, order=Order.desc)
query = query.orderby(sii.item_group, order=Order.desc)
else:
query = apply_group_by_conditions(query, si, sii, filters)
for key, value in (additional_conditions or {}).items():
query = query.where(si[key] == value)
return query
def apply_group_by_conditions(query, si, ii, filters):
if filters.get("group_by") == "Invoice":
query = query.orderby(ii.parent, order=Order.desc)
def apply_order_by_conditions(query, si, ii, filters):
if not filters.get("group_by"):
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
elif filters.get("group_by") == "Invoice":
query += f" order by {ii.parent} desc"
elif filters.get("group_by") == "Item":
query = query.orderby(ii.item_code)
query += f" order by {ii.item_code}"
elif filters.get("group_by") == "Item Group":
query = query.orderby(ii.item_group)
query += f" order by {ii.item_group}"
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc"
return query
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
query = apply_conditions(query, si, sii, filters, additional_conditions)
return query.run(as_dict=True)
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice")
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, si, sii, filters)
return frappe.db.sql(query, params, as_dict=True)
def get_delivery_notes_against_sales_order(item_list):

View File

@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
pi.mode_of_payment,
)
.where(pi.docstatus == 1)
.orderby(pi.posting_date, pi.name, order=Order.desc)
)
if additional_query_columns:
@@ -413,8 +412,17 @@ def get_invoices(filters, additional_query_columns):
filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
)
invoices = query.run(as_dict=True)
return invoices
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Purchase Invoice")
if match_conditions:
query += " and " + match_conditions
query += " order by posting_date desc, name desc"
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype):

View File

@@ -438,7 +438,6 @@ def get_invoices(filters, additional_query_columns):
si.company,
)
.where(si.docstatus == 1)
.orderby(si.posting_date, si.name, order=Order.desc)
)
if additional_query_columns:
@@ -453,8 +452,17 @@ def get_invoices(filters, additional_query_columns):
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
)
invoices = query.run(as_dict=True)
return invoices
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice")
if match_conditions:
query += " and " + match_conditions
query += " order by posting_date desc, name desc"
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype):

View File

@@ -1788,14 +1788,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
):
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_doc(voucher_type, voucher_no)
outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
)
# Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
ref_doc.outstanding_amount = outstanding_amount
frappe.db.set_value(
voucher_type,
voucher_no,
"outstanding_amount",
outstanding["outstanding_in_account_currency"] or 0.0,
outstanding_amount,
)
ref_doc.set_status(update=True)

View File

@@ -69,6 +69,7 @@ class AssetCapitalization(StockController):
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.update_target_asset()
def on_cancel(self):
@@ -82,6 +83,7 @@ class AssetCapitalization(StockController):
self.cancel_target_asset()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
def on_trash(self):

View File

@@ -153,6 +153,48 @@ class AccountsController(TransactionBase):
raise_exception=1,
)
def validate_against_voucher_outstanding(self):
from frappe.model.meta import get_meta
if not get_meta(self.doctype).has_field("outstanding_amount"):
return
if self.get("is_return") and self.return_against and not self.get("is_pos"):
against_voucher_outstanding = frappe.get_value(
self.doctype, self.return_against, "outstanding_amount"
)
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
msg = ""
if self.get("update_outstanding_for_self"):
msg = (
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
"uncheck '{2}' checkbox. <br><br>Or"
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
frappe.bold(_("Update Outstanding for Self")),
)
elif not self.update_outstanding_for_self and (
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
):
self.update_outstanding_for_self = 1
msg = (
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
).format(
against_voucher_outstanding,
get_link_to_form(self.doctype, self.get("return_against")),
flt(abs(self.outstanding_amount)),
)
if msg:
msg += " you can use {} tool to reconcile against {} later.".format(
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
)
frappe.msgprint(_(msg))
def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
@@ -178,6 +220,7 @@ class AccountsController(TransactionBase):
self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate()
self.init_internal_values()
self.validate_against_voucher_outstanding()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -209,20 +252,6 @@ class AccountsController(TransactionBase):
)
)
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
if self.get("update_outstanding_for_self"):
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
frappe.msgprint(
_(
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
frappe.bold("Update Outstanding for Self"),
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
)
)
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
@@ -2100,7 +2129,9 @@ class AccountsController(TransactionBase):
and automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
self.fetch_payment_terms_from_order(po_or_so, doctype)
self.fetch_payment_terms_from_order(
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
)
if self.get("payment_terms_template"):
self.ignore_default_payment_terms_template = 1
elif self.get("payment_terms_template"):
@@ -2141,7 +2172,9 @@ class AccountsController(TransactionBase):
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
else:
self.fetch_payment_terms_from_order(po_or_so, doctype)
self.fetch_payment_terms_from_order(
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
)
self.ignore_default_payment_terms_template = 1
def get_order_details(self):
@@ -2179,7 +2212,9 @@ class AccountsController(TransactionBase):
def linked_order_has_payment_schedule(self, po_or_so):
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
def fetch_payment_terms_from_order(
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
):
"""
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
"""
@@ -2195,12 +2230,25 @@ class AccountsController(TransactionBase):
"invoice_portion": schedule.invoice_portion,
"mode_of_payment": schedule.mode_of_payment,
"description": schedule.description,
"payment_amount": schedule.payment_amount,
"base_payment_amount": schedule.base_payment_amount,
"outstanding": schedule.outstanding,
"paid_amount": schedule.paid_amount,
}
if automatically_fetch_payment_terms:
payment_schedule["payment_amount"] = flt(
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
schedule.precision("payment_amount"),
)
payment_schedule["base_payment_amount"] = flt(
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
schedule.precision("base_payment_amount"),
)
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
else:
payment_schedule["base_payment_amount"] = flt(
schedule.base_payment_amount * self.get("conversion_rate"),
schedule.precision("base_payment_amount"),
)
if schedule.discount_type == "Percentage":
payment_schedule["discount_type"] = schedule.discount_type
payment_schedule["discount"] = schedule.discount
@@ -2459,12 +2507,17 @@ class AccountsController(TransactionBase):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
if not self.conversion_rate:
throw(_("Conversion rate cannot be 0"))
if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))
if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
frappe.msgprint(
_("Conversion rate is 1.00, but document currency is different from company currency")
)
def check_finance_books(self, item, asset):
if (

View File

@@ -801,7 +801,7 @@ class StockController(AccountsController):
child_tab.item_code,
child_tab.qty,
)
.where(parent_tab.docstatus < 2)
.where(parent_tab.docstatus == 1)
)
if self.doctype == "Purchase Invoice":

View File

@@ -373,9 +373,7 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment")
if not rounding_adjustment_computed:
self.doc.rounding_adjustment = 0
self.grand_total_diff = 0
# maintain actual tax rate based on idx
actual_tax_dict = dict(
@@ -440,9 +438,8 @@ class calculate_taxes_and_totals:
and self.discount_amount_applied
and self.doc.discount_amount
and self.doc.apply_discount_on == "Grand Total"
and not rounding_adjustment_computed
):
self.doc.rounding_adjustment = flt(
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"),
)
@@ -528,11 +525,11 @@ class calculate_taxes_and_totals:
return self.adjust_grand_total_for_inclusive_tax()
def adjust_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
# if any inclusive taxes and diff
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
last_tax = self.doc.get("taxes")[-1]
non_inclusive_tax_amount = sum(
flt(d.tax_amount_after_discount_amount)
self.get_tax_amount_if_for_valuation_or_deduction(d.tax_amount_after_discount_amount, d)
for d in self.doc.get("taxes")
if not d.included_in_print_rate
)
@@ -549,27 +546,23 @@ class calculate_taxes_and_totals:
diff = flt(diff, self.doc.precision("rounding_adjustment"))
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.doc.grand_total_diff = diff
else:
self.doc.grand_total_diff = 0
self.grand_total_diff = diff
def calculate_totals(self):
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
self.doc.get("grand_total_diff")
)
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
else:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
self.doc.precision("total_taxes_and_charges"),
)
else:
self.doc.total_taxes_and_charges = 0.0
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
self._set_in_company_currency(self.doc, ["total_taxes_and_charges"])
if self.doc.doctype in [
"Quotation",
@@ -619,7 +612,9 @@ class calculate_taxes_and_totals:
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = self.doc.base_rounded_total = 0
self.doc.rounded_total = 0
self.doc.base_rounded_total = 0
self.doc.rounding_adjustment = 0
return
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
@@ -663,33 +658,29 @@ class calculate_taxes_and_totals:
return
total_for_discount_amount = self.get_total_for_discount_amount()
taxes = self.doc.get("taxes")
net_total = 0
expected_net_total = 0
if total_for_discount_amount:
# calculate item amount after Discount Amount
for i, item in enumerate(self._items):
for item in self._items:
distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
)
item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount"))
adjusted_net_amount = item.net_amount - distributed_amount
expected_net_total += adjusted_net_amount
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
net_total += item.net_amount
# discount amount rounding loss adjustment if no taxes
if (
self.doc.apply_discount_on == "Net Total"
or not taxes
or total_for_discount_amount == self.doc.net_total
) and i == len(self._items) - 1:
discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount,
self.doc.precision("net_total"),
)
# discount amount rounding adjustment
if rounding_difference := flt(
expected_net_total - net_total, self.doc.precision("net_total")
):
item.net_amount = flt(
item.net_amount + discount_amount_loss, item.precision("net_amount")
item.net_amount + rounding_difference, item.precision("net_amount")
)
net_total += rounding_difference
item.net_rate = (
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
@@ -705,20 +696,44 @@ class calculate_taxes_and_totals:
def get_total_for_discount_amount(self):
if self.doc.apply_discount_on == "Net Total":
return self.doc.net_total
else:
actual_taxes_dict = {}
for tax in self.doc.get("taxes"):
if tax.charge_type in ["Actual", "On Item Quantity"]:
tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax)
actual_taxes_dict.setdefault(tax.idx, tax_amount)
elif tax.row_id in actual_taxes_dict:
actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100
actual_taxes_dict.setdefault(tax.idx, actual_tax_amount)
total_actual_tax = 0
actual_taxes_dict = {}
return flt(
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total")
def update_actual_tax_dict(tax, tax_amount):
nonlocal total_actual_tax
if tax.get("add_deduct_tax") == "Deduct":
tax_amount *= -1
if tax.get("category") != "Valuation":
total_actual_tax += tax_amount
actual_taxes_dict[int(tax.idx)] = {
"tax_amount": tax_amount,
"cumulative_tax_amount": total_actual_tax,
}
for tax in self.doc.get("taxes"):
if tax.charge_type in ["Actual", "On Item Quantity"]:
update_actual_tax_dict(tax, tax.tax_amount)
continue
if not tax.row_id:
continue
base_row = actual_taxes_dict.get(int(tax.row_id))
if not base_row:
continue
base_tax_amount = (
base_row["tax_amount"]
if tax.charge_type == "On Previous Row Amount"
else base_row["cumulative_tax_amount"]
)
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
return self.doc.grand_total - total_actual_tax
def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled():
@@ -780,9 +795,12 @@ class calculate_taxes_and_totals:
if (
self.doc.is_return
and self.doc.return_against
and not self.doc.update_outstanding_for_self
and not self.doc.get("is_pos")
or self.is_internal_invoice()
):
# Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
self.doc.outstanding_amount = 0
return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])

View File

@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = """ERP made simple"""
app_icon = "fa fa-th"
app_color = "#e74c3c"
app_email = "info@erpnext.com"
app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """
<span>
Sent via
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
ERPNext
</a>
</span>

View File

@@ -92,7 +92,7 @@ class WorkOrder(Document):
if self.source_warehouse:
self.set_warehouses()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))

View File

@@ -371,4 +371,4 @@ erpnext.patches.v14_0.update_stock_uom_in_work_order_item
erpnext.patches.v14_0.disable_add_row_in_gross_profit
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes

View File

@@ -174,7 +174,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
}
qty(doc, cdt, cdn) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
this.calculate_received_qty(doc, cdt, cdn)
}
super.qty(doc, cdt, cdn);

View File

@@ -336,7 +336,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_taxes() {
var me = this;
this.frm.doc.rounding_adjustment = 0;
this.grand_total_diff = 0;
var actual_tax_dict = {};
// maintain actual tax rate based on idx
@@ -407,7 +407,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
me.frm.doc.rounding_adjustment = flt(me.frm.doc.grand_total -
me.grand_total_diff = flt(me.frm.doc.grand_total -
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
}
}
@@ -519,7 +519,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
adjust_grand_total_for_inclusive_tax() {
var me = this;
// if fully inclusive taxes and diff
// if any inclusive taxes and diff
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
var any_inclusive_tax = false;
$.each(this.frm.doc.taxes || [], function(i, d) {
@@ -530,7 +530,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
function(d) {
if(!d.included_in_print_rate) {
return flt(d.tax_amount_after_discount_amount);
let tax_amount = d.category === "Valuation" ? 0 : d.tax_amount_after_discount_amount;
if (d.add_deduct_tax === "Deduct") tax_amount *= -1;
return tax_amount;
}
}
));
@@ -544,9 +546,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
diff = flt(diff, precision("rounding_adjustment"));
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.frm.doc.grand_total_diff = diff;
} else {
me.frm.doc.grand_total_diff = 0;
me.grand_total_diff = diff;
}
}
}
@@ -557,7 +557,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
: this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -589,9 +589,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
- flt(this.frm.doc.rounding_adjustment), precision("total_taxes_and_charges"));
- this.grand_total_diff, precision("total_taxes_and_charges"));
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges", "rounding_adjustment"]);
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
// Round grand total as per precision
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
@@ -611,6 +611,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0;
this.frm.doc.base_rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
return;
}
@@ -679,22 +680,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
return;
}
var total_for_discount_amount = this.get_total_for_discount_amount();
var net_total = 0;
const total_for_discount_amount = this.get_total_for_discount_amount();
let net_total = 0;
let expected_net_total = 0;
// calculate item amount after Discount Amount
if (total_for_discount_amount) {
$.each(this.frm._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
const adjusted_net_amount = item.net_amount - distributed_amount;
expected_net_total += adjusted_net_amount
item.net_amount = flt(adjusted_net_amount, precision("net_amount", item));
net_total += item.net_amount;
// discount amount rounding loss adjustment if no taxes
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
&& i == (me.frm._items || []).length - 1) {
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss,
precision("net_amount", item));
// discount amount rounding adjustment
// assignment to rounding_difference is intentional
const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
if (rounding_difference) {
item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
net_total += rounding_difference;
}
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
@@ -707,29 +712,38 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total") {
if(this.frm.doc.apply_discount_on == "Net Total")
return this.frm.doc.net_total;
} else {
var total_actual_tax = 0.0;
var actual_taxes_dict = {};
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
actual_taxes_dict[tax.idx] = tax_amount;
} else if (actual_taxes_dict[tax.row_id] !== null) {
var actual_tax_amount = flt(actual_taxes_dict[tax.row_id]) * flt(tax.rate) / 100;
actual_taxes_dict[tax.idx] = actual_tax_amount;
}
});
let total_actual_tax = 0.0;
let actual_taxes_dict = {};
$.each(actual_taxes_dict, function(key, value) {
if (value) total_actual_tax += value;
});
function update_actual_taxes_dict(tax, tax_amount) {
if (tax.add_deduct_tax == "Deduct") tax_amount *= -1;
if (tax.category != "Valuation") total_actual_tax += tax_amount;
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
actual_taxes_dict[tax.idx] = {
tax_amount: tax_amount,
cumulative_total: total_actual_tax
};
}
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
update_actual_taxes_dict(tax, tax.tax_amount);
return;
}
const base_row = actual_taxes_dict[tax.row_id];
if (!base_row) return;
// if charge type is 'On Previous Row Amount', calculate tax on previous row amount
// else (On Previous Row Total) calculate tax on cumulative total
const base_tax_amount = tax.charge_type == "On Previous Row Amount" ? base_row["tax_amount"]: base_row["cumulative_total"];
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
});
return this.frm.doc.grand_total - total_actual_tax;
}
calculate_total_advance(update_paid_amount) {

View File

@@ -297,7 +297,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let d = locals[cdt][cdn];
return {
filters: {
docstatus: 1,
docstatus: ["<", 2],
inspection_type: inspection_type,
reference_name: doc.name,
item_code: d.item_code

View File

@@ -750,7 +750,7 @@ erpnext.utils.update_child_items = function (opts) {
});
}
new frappe.ui.Dialog({
let dialog = new frappe.ui.Dialog({
title: __("Update Items"),
size: "extra-large",
fields: [
@@ -787,7 +787,9 @@ erpnext.utils.update_child_items = function (opts) {
refresh_field("items");
},
primary_action_label: __("Update"),
}).show();
});
dialog.show();
};
erpnext.utils.map_current_doc = function (opts) {

View File

@@ -121,10 +121,10 @@ erpnext.accounts.unreconcile_payment = {
};
let d = new frappe.ui.Dialog({
title: "UnReconcile Allocations",
title: __("UnReconcile Allocations"),
fields: unreconcile_dialog_fields,
size: "large",
primary_action_label: "UnReconcile",
primary_action_label: __("UnReconcile"),
primary_action(values) {
let selected_allocations = values.allocations.filter((x) => x.__checked);
if (selected_allocations.length > 0) {
@@ -138,7 +138,7 @@ erpnext.accounts.unreconcile_payment = {
);
d.hide();
} else {
frappe.msgprint("No Selection");
frappe.msgprint(__("No Selection"));
}
},
});

View File

@@ -1,4 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
{% if country != "United States" %}{{ country }}{% endif -%}

View File

@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
frm.add_fetch("lead_name", "company_name", "customer_name");
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
frm.set_query("customer_group", { is_group: 0 });
frm.set_query("default_price_list", { selling: 1 });
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
invoices_by_customer = frappe.db.get_all(
invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields,
)
invoices_by_name = frappe.db.get_all(
invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields,
@@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name
elif status:
invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields)
invoice_list = frappe.db.get_list("POS Invoice", filters={"status": status}, fields=fields)
return invoice_list

View File

@@ -190,35 +190,42 @@ class TransactionDeletionRecord(Document):
"""Delete addresses to which leads are linked"""
self.validate_doc_status()
if not self.delete_leads_and_addresses:
leads = frappe.get_all("Lead", filters={"company": self.company})
leads = ["'%s'" % row.get("name") for row in leads]
leads = frappe.db.get_all("Lead", filters={"company": self.company}, pluck="name")
addresses = []
if leads:
addresses = frappe.db.sql_list(
"""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(leads=",".join(leads))
addresses = frappe.db.get_all(
"Dynamic Link", filters={"link_name": ("in", leads)}, pluck="parent"
)
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql(
"""delete from `tabAddress` where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))
)
address = qb.DocType("Address")
dl1 = qb.DocType("Dynamic Link")
dl2 = qb.DocType("Dynamic Link")
frappe.db.sql(
"""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))
)
qb.from_(address).delete().where(
(address.name.isin(addresses))
& (
address.name.notin(
qb.from_(dl1)
.join(dl2)
.on((dl1.parent == dl2.parent) & (dl1.link_doctype != dl2.link_doctype))
.select(dl1.parent)
.distinct()
)
)
).run()
dynamic_link = qb.DocType("Dynamic Link")
qb.from_(dynamic_link).delete().where(
(dynamic_link.link_doctype == "Lead")
& (dynamic_link.parenttype == "Address")
& (dynamic_link.link_name.isin(leads))
).run()
customer = qb.DocType("Customer")
qb.update(customer).set(customer.lead_name, None).where(customer.lead_name.isin(leads)).run()
frappe.db.sql(
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
leads=",".join(leads)
)
)
self.db_set("delete_leads_and_addresses", 1)
self.enqueue_task(task="Reset Company Values")

View File

@@ -15,7 +15,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
<a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
def after_install():

View File

@@ -196,8 +196,17 @@ class InventoryDimension(Document):
custom_fields["Stock Ledger Entry"] = dimension_field
filter_custom_fields = {}
ignore_doctypes = [
"Pick List Item",
"Maintenance Visit Purpose",
]
if custom_fields:
for doctype, fields in custom_fields.items():
if doctype in ignore_doctypes:
continue
if isinstance(fields, dict):
fields = [fields]

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on("Pick List", {
after_save(frm) {
setTimeout(() => {
// Added to fix the issue of locations table not getting updated after save
frm.reload_doc();
}, 500);
},
setup: (frm) => {
frm.set_indicator_formatter("item_code", function (doc) {
return doc.stock_qty === 0 ? "red" : "green";

View File

@@ -25,10 +25,38 @@ from erpnext.stock.get_item_details import get_conversion_factor
class PickList(Document):
def validate(self):
self.validate_expired_batches()
self.validate_for_qty()
self.validate_stock_qty()
self.check_serial_no_status()
def validate_expired_batches(self):
batches = []
for row in self.get("locations"):
if row.get("batch_no") and row.get("picked_qty"):
batches.append(row.batch_no)
if batches:
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(batch)
.select(batch.name)
.where(
(batch.name.isin(batches))
& (batch.expiry_date <= frappe.utils.nowdate())
& (batch.expiry_date.isnotnull())
)
)
expired_batches = query.run(as_dict=True)
if expired_batches:
msg = "<ul>" + "".join(f"<li>{batch.name}</li>" for batch in expired_batches) + "</ul>"
frappe.throw(
_("The following batches are expired, please restock them: <br> {0}").format(msg),
title=_("Expired Batches"),
)
def before_save(self):
self.update_status()
if not self.pick_manually:
@@ -271,6 +299,7 @@ class PickList(Document):
self.remove(row)
updated_locations = frappe._dict()
len_idx = len(self.get("locations")) or 0
for item_doc in items:
item_code = item_doc.item_code
@@ -313,6 +342,8 @@ class PickList(Document):
if location.picked_qty > location.stock_qty:
location.picked_qty = location.stock_qty
len_idx += 1
location.idx = len_idx
self.append("locations", location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
@@ -321,6 +352,9 @@ class PickList(Document):
for location in locations_replica:
location.stock_qty = 0
location.picked_qty = 0
len_idx += 1
location.idx = len_idx
self.append("locations", location)
frappe.msgprint(
_(
@@ -430,9 +464,11 @@ class PickList(Document):
pi_item.item_code,
pi_item.warehouse,
pi_item.batch_no,
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
"picked_qty"
),
Sum(
Case()
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
.else_(pi_item.stock_qty)
).as_("picked_qty"),
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
)
.where(
@@ -465,8 +501,32 @@ class PickList(Document):
else:
picked_items[item_data.item_code][key] = data
self.update_picked_item_from_current_pick_list(picked_items)
return picked_items
def update_picked_item_from_current_pick_list(self, picked_items):
for row in self.get("locations"):
if flt(row.picked_qty) > 0:
key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse
serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None
if row.item_code not in picked_items:
picked_items[row.item_code] = {}
if key not in picked_items[row.item_code]:
picked_items[row.item_code][key] = frappe._dict(
{
"picked_qty": 0,
"serial_no": [],
"batch_no": row.batch_no or "",
"warehouse": row.warehouse,
}
)
picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty)
if serial_no:
picked_items[row.item_code][key]["serial_no"].extend(serial_no)
def _get_product_bundles(self) -> dict[str, str]:
# Dict[so_item_row: item_code]
product_bundles = {}

View File

@@ -865,15 +865,19 @@ def get_billed_amount_against_po(po_items):
if not po_items:
return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice_item.docstatus == 1)
& (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
& (purchase_invoice.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)

View File

@@ -2731,6 +2731,36 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(return_pr.per_billed, 100)
self.assertEqual(return_pr.status, "Completed")
def test_pr_status_based_on_invoices_with_update_stock(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as _make_purchase_invoice,
)
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt as _make_purchase_receipt,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
item_code = "Test Item for PR Status Based on Invoices"
create_item(item_code)
po = create_purchase_order(item_code=item_code, qty=10)
pi = _make_purchase_invoice(po.name)
pi.update_stock = 1
pi.items[0].qty = 5
pi.submit()
po.reload()
self.assertEqual(po.per_billed, 50)
pr = _make_purchase_receipt(po.name)
self.assertEqual(pr.items[0].qty, 5)
pr.submit()
pr.reload()
self.assertEqual(pr.status, "To Bill")
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -49,6 +49,9 @@ class QualityInspection(Document):
child = self.append("readings", {})
child.update(d)
child.status = "Accepted"
child.parameter_group = frappe.get_value(
"Quality Inspection Parameter", d.specification, "parameter_group"
)
@frappe.whitelist()
def get_quality_inspection_template(self):

View File

@@ -209,7 +209,7 @@ class SerialNo(StockController):
OR serial_no like %s
)
ORDER BY
posting_date desc, posting_time desc, creation desc""",
posting_datetime desc, creation desc""",
(
self.item_code,
self.company,
@@ -279,7 +279,14 @@ def validate_serial_no(sle, item_det):
_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)
)
if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)):
if (
(
(sle.voucher_type == "Stock Reconciliation" and sle.actual_qty > 0)
or sle.voucher_type != "Stock Reconciliation"
)
and len(serial_nos)
and len(serial_nos) != abs(cint(sle.actual_qty))
):
frappe.throw(
_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
abs(sle.actual_qty), sle.item_code, len(serial_nos)

View File

@@ -1314,17 +1314,38 @@ class StockEntry(StockController):
@frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
item = frappe.db.sql(
"""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
id.expense_account, id.buying_cost_center
from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
where i.name=%s
and i.disabled=0
and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""",
(self.company, args.get("item_code"), nowdate()),
as_dict=1,
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
query = (
frappe.qb.from_(item)
.left_join(item_default)
.on((item.name == item_default.parent) & (item_default.company == self.company))
.select(
item.name,
item.stock_uom,
item.description,
item.image,
item.item_name,
item.item_group,
item.has_batch_no,
item.sample_quantity,
item.has_serial_no,
item.allow_alternative_item,
item_default.expense_account,
item_default.buying_cost_center,
)
.where(
(item.name == args.get("item_code"))
& (item.disabled == 0)
& (
(item.end_of_life.isnull())
| (item.end_of_life < "1900-01-01")
| (item.end_of_life > nowdate())
)
)
)
item = query.run(as_dict=True)
if not item:
frappe.throw(
@@ -1369,6 +1390,11 @@ class StockEntry(StockController):
if self.purpose == "Material Issue":
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
if self.purpose == "Manufacture":
ret["expense_account"] = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
)
for company_field, field in {
"stock_adjustment_account": "expense_account",
"cost_center": "cost_center",

View File

@@ -0,0 +1,9 @@
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
on_doctype_update as create_sle_indexes,
)
def execute():
"""Ensure SLE Indexes"""
create_sle_indexes()

View File

@@ -81,5 +81,11 @@ frappe.query_reports["Stock Ageing"] = {
fieldtype: "Check",
default: 0,
},
{
fieldname: "ignore_closing_balance",
label: __("Ignore Closing Balance"),
fieldtype: "Check",
default: 0,
},
],
};

View File

@@ -6,7 +6,8 @@ from operator import itemgetter
import frappe
from frappe import _
from frappe.utils import cint, date_diff, flt, get_datetime
from frappe.query_builder import Order
from frappe.utils import add_days, cint, date_diff, flt, get_date_str, get_datetime, getdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -49,7 +50,13 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
latest_age = date_diff(to_date, fifo_queue[-1][1])
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
row = [
details.name or details.item_code,
details.item_name,
details.description,
details.item_group,
details.brand,
]
if filters.get("show_warehouse_wise_stock"):
row.append(details.warehouse)
@@ -217,6 +224,67 @@ class FIFOSlots:
self.filters = filters
self.sle = sle
def get_closing_balance(self):
if self.filters.get("ignore_closing_balance"):
return []
if (
self.filters.get("item_code")
or self.filters.get("warehouse")
or self.filters.get("warehouse_type")
):
return
if self.sle:
return
table = frappe.qb.DocType("Closing Stock Balance")
query = (
frappe.qb.from_(table)
.select(table.name, table.to_date)
.where(
(table.docstatus == 1)
& (table.company == self.filters.company)
& (table.to_date < self.filters.get("to_date"))
& (table.status == "Completed")
)
.orderby(table.to_date, order=Order.desc)
.limit(1)
)
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.filters.get(fieldname):
query = query.where(table[fieldname] == self.filters.get(fieldname))
return query.run(as_dict=True)
def prepare_stock_ageing_from_stock_closing_balance(self):
closing_balance = self.get_closing_balance()
if not closing_balance:
return
self.start_from = add_days(closing_balance[0].to_date, 1)
closing_data = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data()
stock_ledger_entries = closing_data.get("data")
for d in stock_ledger_entries:
if isinstance(d, dict):
d = frappe._dict(d)
d.actual_qty = d.bal_qty
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
serial_nos = d.serial_no if d.serial_no else []
if fifo_queue and isinstance(fifo_queue[0][0], str):
d.has_serial_no = 1
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
self.__update_balances(d, key)
def generate(self) -> dict:
"""
Returns dict of the foll.g structure:
@@ -227,6 +295,9 @@ class FIFOSlots:
consumed/updated and maintained via FIFO. **
}
"""
self.start_from = None
self.prepare_stock_ageing_from_stock_closing_balance()
stock_ledger_entries = self.sle
_system_settings = frappe.get_cached_doc("System Settings")
@@ -259,15 +330,32 @@ class FIFOSlots:
return self.item_details
def format_fifo_queue(self, fifo_queue: list) -> list:
if not fifo_queue:
return []
fifo_queue = [[x[0], getdate(x[1])] for x in fifo_queue]
return fifo_queue
def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."
key = (row.name, row.warehouse)
self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
fifo_queue = self.item_details[key]["fifo_queue"]
if not row.name:
key = (row.item_code, row.warehouse)
else:
key = (row.name, row.warehouse)
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
self.transferred_item_details.setdefault(transferred_item_key, [])
if key not in self.item_details:
row.fifo_queue = self.format_fifo_queue(row.fifo_queue)
self.item_details.setdefault(key, {"details": row, "fifo_queue": row.fifo_queue or []})
fifo_queue = self.item_details[key]["fifo_queue"]
transferred_item_key = None
if row.voucher_no:
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
self.transferred_item_details.setdefault(transferred_item_key, [])
return key, fifo_queue, transferred_item_key
@@ -351,10 +439,10 @@ class FIFOSlots:
transfer_qty_to_pop = 0
def __update_balances(self, row: dict, key: tuple | str):
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or row.bal_qty
if "total_qty" not in self.item_details[key]:
self.item_details[key]["total_qty"] = row.actual_qty
self.item_details[key]["total_qty"] = row.actual_qty or row.bal_qty
else:
self.item_details[key]["total_qty"] += row.actual_qty
@@ -417,6 +505,10 @@ class FIFOSlots:
)
)
if self.start_from:
from_date = get_datetime(get_date_str(self.start_from) + " 00:00:00")
sle_query = sle_query.where(sle.posting_datetime >= from_date)
if self.filters.get("warehouse"):
sle_query = self.__get_warehouse_conditions(sle, sle_query)
elif self.filters.get("warehouse_type"):

View File

@@ -1 +1 @@
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"type": "git",
"url": "git+https://github.com/frappe/erpnext.git"
},
"homepage": "https://erpnext.com",
"homepage": "https://frappe.io/erpnext",
"author": "Frappe Technologies Pvt. Ltd.",
"license": "GPL-3.0",
"bugs": {