Compare commits

...

131 Commits

Author SHA1 Message Date
Deepesh Garg
a3b8735222 fix: Values with same account and different account number in consolidated balance sheet report (#27493)
(cherry picked from commit 625626b973)

# Conflicts:
#	erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
2025-02-24 00:11:56 +00:00
mergify[bot]
f5160dc83d fix: use Stock Qty while getting POS Reserved Qty (backport #38962) (#38983)
fix: use `Stock Qty` while getting `POS Reserved Qty`

(cherry picked from commit 7223106417)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-28 12:00:04 +05:30
ruthra kumar
ab82e30fac Merge pull request #38947 from frappe/mergify/bp/version-13-hotfix/pr-38891
fix: incorrect price list in customer-wise item price report (backport #38891)
2023-12-26 12:16:39 +05:30
ruthra kumar
32f3365ac7 fix: incorrect price list in customer-wise item price report
(cherry picked from commit 9a00edb031)
2023-12-26 06:18:00 +00:00
Deepesh Garg
80012b7339 Merge pull request #38769 from frappe/deepeshgarg007-patch-1
chore: Loan Interest Accrual post topup
2023-12-22 08:40:48 +05:30
ruthra kumar
b049b52294 Merge pull request #38750 from barredterra/pe-empty-referneces
fix(Payment Entry): don't check empty references
2023-12-21 11:00:21 +05:30
Rucha Mahabal
5d1c35634c Merge pull request #38610 from ruchamahabal/EL-overallocation-v13
fix: earned leave exceeding annual allocation
2023-12-19 15:26:38 +05:30
Rucha Mahabal
9900274b27 test(fix): earned leave tests 2023-12-19 13:03:57 +05:30
Rucha Mahabal
14955c70d4 fix: earned leave allocation in policy assignment 2023-12-19 13:03:36 +05:30
Raffael Meyer
8c55e35d20 Merge pull request #38676 from frappe/mergify/bp/version-13-hotfix/pr-38672
fix: get data for leaderboard (backport #38672)
2023-12-15 16:27:22 +01:00
barredterra
e6e9f1dc26 chore: remove deprecation wrapper
not available in v13
2023-12-15 15:46:40 +01:00
Deepesh Garg
4f8b13ac57 chore: Loan Interest Accrual post topup 2023-12-15 12:46:59 +05:30
barredterra
f0877ffa47 fix(Payment Entry): don't check empty references
Manual partial backport of https://github.com/frappe/erpnext/pull/36649
2023-12-14 13:39:35 +01:00
barredterra
e291b5db3d chore: deprecate unused method
(cherry picked from commit 956c3c50a0)
2023-12-12 04:24:54 +00:00
barredterra
b0f7de1a0f fix: get sales partner for leaderboard
(cherry picked from commit 40c1acc961)
2023-12-12 04:24:54 +00:00
barredterra
8dbb200fe3 fix: get sales person for leaderboard
(cherry picked from commit 7babfd4ac4)
2023-12-12 04:24:54 +00:00
barredterra
7df8425756 fix: get suppliers for leaderboard
(cherry picked from commit 65df4b6aa8)
2023-12-12 04:24:54 +00:00
barredterra
3863c4e7fb fix: get items for leaderboard
(cherry picked from commit 2721ee3a8d)
2023-12-12 04:24:53 +00:00
barredterra
10f02e60ce fix: get customers for leaderboard
(cherry picked from commit 137b5a6108)
2023-12-12 04:24:53 +00:00
Rucha Mahabal
48eaa51c4a test: earned leave over-allocation 2023-12-07 13:04:51 +05:30
Rucha Mahabal
fee4eae96c fix: avoid over allocation during backdated assignment creation 2023-12-07 13:04:23 +05:30
Rucha Mahabal
ee7c9add39 fix: earned leave exceeding annual allocation 2023-12-07 13:01:57 +05:30
mergify[bot]
1b78dd17c9 fix: consider the Valuation Method while picking incorrect SLE (backport #38592) (#38596)
* fix: incorrect SLE for `Moving Average` valuation method

(cherry picked from commit 8beec58670)

* feat: add `Valuation Method` column in `Stock Ledger Variance` report

(cherry picked from commit 16c297c2ec)

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-05 18:14:19 +05:30
mergify[bot]
77e01ebacf feat: Company filter in Stock Ledger Variance report (backport #38553) (#38575)
feat: `Company` filter in `Stock Ledger Variance` report

(cherry picked from commit fb3421fcce)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-05 12:32:57 +05:30
mergify[bot]
bd371e697c fix: make create button translatable (backport #38165) (#38412)
fix: make create button translatable (#38165)

* fix: make all create buttons translatable

* style: use double quotes

---------

Co-authored-by: PatrickDenis-stack <77415730+PatrickDenis-stack@users.noreply.github.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
(cherry picked from commit 8e4b591ea2)

Co-authored-by: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com>
2023-12-01 11:33:35 +05:30
Raffael Meyer
2c4cee025b ci: pin semgrep to old version (#38426)
* ci: pin semgrep to old version

current version has problem with PRs originating from fork

* ci: unpin semgrep

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
2023-11-29 22:06:31 +05:30
Deepesh Garg
303becf1e3 Merge pull request #38390 from GursheenK/payment-reco-currency-symbol
fix: currency symbol in payment rec allocations
2023-11-29 17:22:12 +05:30
Gursheen Anand
6f44a1630f fix: show difference amount in default currency 2023-11-28 16:31:40 +05:30
Gursheen Anand
b7944a7c07 chore: hide currency column 2023-11-28 15:51:33 +05:30
Gursheen Anand
3dfc1450a1 fix: update voucher currency for allocated entry 2023-11-28 15:43:36 +05:30
Gursheen Anand
835c85a087 feat: add currency column for allocated entries 2023-11-28 15:42:30 +05:30
Gursheen Kaur Anand
190f77abff fix: precision for invoice outstandings (#38323)
fix: precision while setting inv outstanding
2023-11-26 18:56:34 +05:30
mergify[bot]
095d99dbd2 fix: patch - Duplicate entry quality inspection parameter (backport #38262) (#38265)
fix: patch - Duplicate entry quality inspection parameter (#38262)

(cherry picked from commit 0ca7527f7a)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-11-22 14:09:00 +05:30
mergify[bot]
a9429e160d fix(Timesheet): warn user if billing hours > actual hours instead of resetting (backport #38239) (#38242)
fix(Timesheet): warn user if billing hours > actual hours instead of resetting  (#38239)

* revert: "fix(Timesheet): reset billing hours equal to hours if they exceed actual hours"

This reverts commit 0ec8034507.

* fix: warn user if billing hours > actual hours

(cherry picked from commit ac91030b31)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2023-11-21 13:42:36 +05:30
Sagar Vora
5342cd0dfa Merge pull request #38170 from frappe/mergify/bp/version-13-hotfix/pr-38167
fix: pass check permission in `render_address` (backport #38167)
2023-11-18 17:43:40 +05:30
Sagar Vora
3bf84e1464 chore: fix conflicts 2023-11-18 17:42:06 +05:30
Vishakh Desai
65ae8d9c05 fix: pass check permission in render_address
(cherry picked from commit 45299fe4b3)

# Conflicts:
#	erpnext/accounts/party.py
2023-11-18 12:09:18 +00:00
Rucha Mahabal
35717124cd Merge pull request #38158 from ruchamahabal/perf-leave-balance-report-v13
perf: faster Employee Leave Balance report
2023-11-17 22:57:49 +05:30
Rucha Mahabal
89c107ea8b chore: remove states from json 2023-11-17 22:56:42 +05:30
Rucha Mahabal
958db77cda fix: backward compatible query without pluck 2023-11-17 21:11:42 +05:30
Rucha Mahabal
bc1da4678a refactor: retain backward compatible type hints 2023-11-17 20:45:34 +05:30
Rucha Mahabal
6cb8a40339 fix: get_previous_allocation return value 2023-11-17 20:19:39 +05:30
Rucha Mahabal
9139c14639 refactor(Leave Balance Summary report): remove unused department approver queries 2023-11-17 20:19:10 +05:30
Rucha Mahabal
461eb7a50d refactor: rewrite queries with frappe.qb + refactor type hints 2023-11-17 20:17:31 +05:30
Rucha Mahabal
635c3d54f5 perf: limit rows to 1 for cf leave expiry query 2023-11-17 20:17:11 +05:30
Rucha Mahabal
1bd3f4eeef refactor: remove unnecessary approver queries 2023-11-17 20:16:58 +05:30
Rucha Mahabal
4b8ed0f6ae fix: use get_all instead of get_list
- query report already filters records based on permissions for link fields

- allow employees access to leave balance report by default
2023-11-17 20:16:43 +05:30
Rucha Mahabal
eea7bbcea7 perf: index most queried fields in Leave Ledger Entry 2023-11-17 20:16:24 +05:30
mergify[bot]
1e436052e2 fix(Timesheet): reset billing hours equal to hours if they exceed actual hours (backport #38134) (#38154)
fix(Timesheet): reset billing hours equal to hours if they exceed actual hours (#38134)

(cherry picked from commit 20c6e9fca2)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2023-11-17 18:32:35 +05:30
mergify[bot]
5be5fde276 fix: close Credit Limit Crossed dialog (backport #38052) (#38057)
fix: close `Credit Limit Crossed` dialog (#38052)

(cherry picked from commit 94faa44697)

Co-authored-by: Arjun <arjun99c@gmail.com>
2023-11-12 18:07:50 +05:30
mergify[bot]
8cb0f690d5 fix: remove voucher type and no for Item and Warehouse based reposting (backport #37849) (backport #37850) (#37938)
* fix: remove voucher type and no for Item and Warehouse based reposting (backport #37849) (#37850)

* fix: remove voucher type and no for Item and Warehouse based reposting

(cherry picked from commit 0104897d69)

* chore: fix test cases

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
(cherry picked from commit e19cade12d)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
#	erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
#	erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json

* chore: fix conflicts

* chore: fix conflicts

* chore: fix conflicts

* fix: conflicts

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-11-07 16:07:17 +05:30
mergify[bot]
4789ecacea fix: Quality Inspection Parameter migration - DuplicateEntryError due to case sensitivity (backport #37499) (#37918)
fix: Quality Inspection Parameter migration - DuplicateEntryError due to case sensitivity (#37499)

* fix: account for case-insensitive database primary key for parameter names

* chore: linting

(cherry picked from commit b099590b2c)

Co-authored-by: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com>
2023-11-05 06:22:42 +00:00
Anand Baburajan
d00257ffd7 Merge pull request #37891 from frappe/mergify/bp/version-13-hotfix/pr-37889
fix: add missing disbursement account in update_old_loans patch (backport #37889)
2023-11-03 21:48:57 +05:30
anandbaburajan
37b1a0e778 fix: add missing disbursement account in update_old_loans patch
(cherry picked from commit ad64065ec6)
2023-11-03 16:17:29 +00:00
mergify[bot]
6952f0f082 fix: permission error while creating Supplier Quotation from Portal (backport #37864) (#37872)
* fix: permission error while creating Supplier Quotation from Portal

(cherry picked from commit e019d43d0b)

# Conflicts:
#	erpnext/controllers/buying_controller.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-11-03 16:26:45 +05:30
Dany Robert
8f4ded6ad1 fix: e-invoice jwt verification error (#37818) 2023-11-01 13:27:24 +05:30
Ankush Menat
13d5eec194 fix: bump pygithub requirement to handle conflict (#37800)
build: bump pygithub requirement

This conflicts with pyjwt
2023-10-31 19:55:56 +05:30
Raffael Meyer
335b6c84db Merge pull request #37666 from frappe/mergify/bp/version-13-hotfix/pr-37658
fix: wrong german translation (backport #37658)
2023-10-28 23:07:59 +02:00
barredterra
00dff0a219 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-37658 2023-10-28 22:42:43 +02:00
mergify[bot]
b55b428114 chore: fixed test case non_internal_transfer_delivery_note (backport #37671) (#37677)
chore: fixed test case non_internal_transfer_delivery_note (#37671)

(cherry picked from commit 2bcff4c7f2)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-25 13:55:17 +05:30
Raffael Meyer
5669a89afe fix: wrong german translation (#37658)
(cherry picked from commit fa5780ca81)
2023-10-25 03:44:15 +00:00
mergify[bot]
b5b51879ee fix: remove from or target warehouse for non internal transfer entries (backport #37612) (#37629)
* fix: remove from or target warehouse for non internal transfer entries (#37612)

(cherry picked from commit 5136fe196b)

* chore: removed test cases

* chore: removed test case

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-24 08:41:41 +05:30
mergify[bot]
4fc45b035a fix: incorrect cost center in the purchase invoice (backport #37591) (#37610)
* fix: incorrect cost center in the purchase invoice (#37591)

(cherry picked from commit 14b009b093)

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

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-20 17:03:25 +05:30
mergify[bot]
bc907b22d4 fix: Issues related to RFQ and Supplier Quotation on Portal (backport #37565) (backport #37577) (#37588)
* fix: Issues related to RFQ and Supplier Quotation on Portal (backport #37565) (#37577)

* fix: Issues related to RFQ and Supplier Quotation on Portal (#37565)

fix: RFQ and Supplier Quotation for Portal
(cherry picked from commit 2851a41310)

* chore: removed backport changes

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit e1504efd40)

# Conflicts:
#	erpnext/templates/includes/rfq.js

* chore: fix conflicts

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-19 16:39:38 +05:30
mergify[bot]
7e26449b9f chore: rewrite query using query builder (backport #37310) (#37585)
* chore: rewrite query using query builder

(cherry picked from commit 25718f5cc7)

* chore: fix shopping cart tests

(cherry picked from commit fb51cae88b)

* chore: fix test case

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-19 15:58:45 +05:30
mergify[bot]
49d0ab5867 fix: e-commerce permissions for address (backport #37554) (#37560)
* fix: E-commerce permissions

(cherry picked from commit f4d74990fe)

# Conflicts:
#	erpnext/accounts/party.py
#	erpnext/controllers/selling_controller.py

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-10-18 18:46:10 +05:30
Deepesh Garg
ddec91202a Merge branch 'version-13' into version-13-hotfix 2023-10-17 17:59:36 +05:30
Ankush Menat
fe681acaad Merge pull request #37534 from frappe/mergify/bp/version-13-hotfix/pr-37532
fix: keep customer/supplier website role by default (backport #37532)
2023-10-16 17:36:59 +05:30
Ankush Menat
52aff1f703 fix: keep customer/supplier website role by default
(cherry picked from commit d2096cfdb7)
2023-10-16 17:35:44 +05:30
ruthra kumar
1cfc6cfe5d Merge pull request #37509 from frappe/mergify/bp/version-13-hotfix/pr-37435
fix(gp): wrong `allocated_amount` when grouped by Sales Person (backport #37435)
2023-10-15 10:30:28 +05:30
Dany Robert
7805c3acf6 fix(gp): wrong allocated_amount on multi sales person invoice
(cherry picked from commit bda82bf1e9)
2023-10-15 08:14:26 +05:30
mergify[bot]
8b4e69235a fix: use flt to ignore TypeError (backport #37481) (#37489)
fix: use `flt` to ignore TypeError (#37481)

(cherry picked from commit d2b22db500)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-13 10:25:28 +05:30
Frappe PR Bot
35a62e2e8d chore(release): Bumped to Version 13.54.4
## [13.54.4](https://github.com/frappe/erpnext/compare/v13.54.3...v13.54.4) (2023-10-11)

### Bug Fixes

* negative valuation rate in PR return (backport [#37424](https://github.com/frappe/erpnext/issues/37424)) (backport [#37462](https://github.com/frappe/erpnext/issues/37462)) ([#37463](https://github.com/frappe/erpnext/issues/37463)) ([f6b3532](f6b35324ef))
2023-10-11 14:24:45 +00:00
mergify[bot]
f6b35324ef fix: negative valuation rate in PR return (backport #37424) (backport #37462) (#37463)
fix: negative valuation rate in PR return (backport #37424) (#37462)

* fix: negative valuation rate in PR return (#37424)

* fix: negative valuation rate in PR return

* test: add test case for PR return

(cherry picked from commit 26ad688584)

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

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
(cherry picked from commit 66ad823417)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-10-11 19:53:10 +05:30
mergify[bot]
66ad823417 fix: negative valuation rate in PR return (backport #37424) (#37462)
* fix: negative valuation rate in PR return (#37424)

* fix: negative valuation rate in PR return

* test: add test case for PR return

(cherry picked from commit 26ad688584)

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

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-11 19:17:02 +05:30
ruthra kumar
f674635ecf Merge pull request #37440 from ruthra-kumar/patch_for_exchange_rate_service_provider
chore: patch to set default exchange rate service providers
2023-10-10 21:02:10 +05:30
Frappe PR Bot
0000c38563 chore(release): Bumped to Version 13.54.3
## [13.54.3](https://github.com/frappe/erpnext/compare/v13.54.2...v13.54.3) (2023-10-10)

### Bug Fixes

* change currency exchange API to api.frankfurter.app ([76919c4](76919c4af2))
2023-10-10 14:47:51 +00:00
Deepesh Garg
23f12f6463 Merge pull request #37432 from frappe/version-13-hotfix
chore: release v13
2023-10-10 20:16:20 +05:30
ruthra kumar
6f6db432b5 chore: patch to set default exchange rate service provider 2023-10-10 17:37:04 +05:30
ruthra kumar
ba7a822682 Merge pull request #37389 from frappe/mergify/bp/version-13/pr-37385
refactor: multiple service providers for Currency Exchange rates (backport #37385)
2023-10-07 19:28:53 +05:30
ruthra kumar
32ae68eb5c refactor: access_key field for service_provider
(cherry picked from commit 22fb65621c)
2023-10-07 12:46:52 +00:00
ruthra kumar
9eb02637d8 refactor: selectable exchange rate service providers
(cherry picked from commit 55dfd8e995)
2023-10-07 12:46:52 +00:00
ruthra kumar
fd8543ebd3 Merge pull request #37385 from ruthra-kumar/multiple_currency_exchange_provider
refactor: multiple service providers for Currency Exchange rates
2023-10-07 15:34:05 +05:30
ruthra kumar
22fb65621c refactor: access_key field for service_provider 2023-10-07 14:28:32 +05:30
ruthra kumar
55dfd8e995 refactor: selectable exchange rate service providers 2023-10-06 15:05:57 +05:30
Frappe PR Bot
fa0eef96b9 chore(release): Bumped to Version 13.54.1
## [13.54.1](https://github.com/frappe/erpnext/compare/v13.54.0...v13.54.1) (2023-10-05)

### Bug Fixes

* change currency exchange API to api.frankfurter.app ([1da808a](1da808a125))
2023-10-05 03:51:29 +00:00
ruthra kumar
ee42b0a16b Merge pull request #37365 from frappe/mergify/bp/version-13/pr-37355
fix: change currency exchange API to api.frankfurter.app (backport #37355)
2023-10-05 09:20:03 +05:30
ruthra kumar
40e475836e refactor(test): update exchange rate based on 'frankfurter' provider
(cherry picked from commit a3fd4db450)
2023-10-05 03:14:51 +00:00
dsnetprofitxl
1da808a125 fix: change currency exchange API to api.frankfurter.app
(cherry picked from commit 76919c4af2)
2023-10-05 03:14:49 +00:00
ruthra kumar
c86cd99395 Merge pull request #37355 from dsnetprofitxl/fix-exchange-rate
fix: change currency exchange API to api.frankfurter.app
2023-10-04 21:58:13 +05:30
ruthra kumar
a3fd4db450 refactor(test): update exchange rate based on 'frankfurter' provider 2023-10-04 21:33:11 +05:30
dsnetprofitxl
76919c4af2 fix: change currency exchange API to api.frankfurter.app 2023-10-04 16:01:22 +05:30
Frappe PR Bot
b0708d29a8 chore(release): Bumped to Version 13.54.0
# [13.54.0](https://github.com/frappe/erpnext/compare/v13.53.0...v13.54.0) (2023-10-04)

### Bug Fixes

* incorrect stock ledger entries in DN (backport [#36944](https://github.com/frappe/erpnext/issues/36944)) ([#37067](https://github.com/frappe/erpnext/issues/37067)) ([5833c4d](5833c4dae2))
* trial balance report freezes when adding filters (backport [#37264](https://github.com/frappe/erpnext/issues/37264)) ([#37267](https://github.com/frappe/erpnext/issues/37267)) ([ff6b38c](ff6b38c9e7))
* Update `advance_paid` in SO/PO after unlinking from advance entry ([a6bef64](a6bef64c8e))

### Features

* `Stock Ledger Variance` report (backport [#37165](https://github.com/frappe/erpnext/issues/37165)) ([#37184](https://github.com/frappe/erpnext/issues/37184)) ([5092ea1](5092ea175e))
* provision to make reposting entries from Stock and Account Value Comparison Report (backport [#35365](https://github.com/frappe/erpnext/issues/35365)) ([#37171](https://github.com/frappe/erpnext/issues/37171)) ([48eb6a6](48eb6a6573))
2023-10-04 04:15:19 +00:00
ruthra kumar
caa8417306 Merge pull request #37340 from frappe/version-13-hotfix
chore: release v13
2023-10-04 09:43:53 +05:30
mergify[bot]
ff6b38c9e7 fix: trial balance report freezes when adding filters (backport #37264) (#37267)
fix: trial balance report freezes when adding filters (#37264)

fix: Only add onclick if correct data is returned

workaround for https://github.com/frappe/datatable/issues/177

(cherry picked from commit 2dc95e5d59)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-09-27 12:37:10 +05:30
Frappe PR Bot
4285bbcdc0 chore: release v13 (#37245)
* feat: provision to make reposting entries from Stock and Account Value Comparison Report (backport #35365) (#37171)

* feat: provision to make reposting entries from Stock and Account Value Comparison Report

(cherry picked from commit 7b818e9d34)

* fix: `linter`

* fix(ux): throw if no row selected to create repost entries

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* fix: incorrect stock ledger entries in DN (backport #36944) (#37067)

* fix: incorrect stock ledger entries in DN (#36944)

(cherry picked from commit 0e83190c19)

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

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>

* feat: `Stock Ledger Variance` report (backport #37165) (#37184)

feat: `Stock Ledger Variance` report (#37165)

* feat: `Stock Ledger Variance` report

* refactor: `get_data()`

(cherry picked from commit acda72d616)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* fix: Update `advance_paid` in SO/PO after unlinking from advance entry

(cherry picked from commit 426350eee6)

* test: Impact on SO of advance PE submit and unlinking/replacement by SI

(cherry picked from commit 8a4954d713)

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
Co-authored-by: marination <maricadsouza221197@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-09-27 09:25:47 +05:30
ruthra kumar
b6dc47ec8a Merge pull request #37197 from frappe/mergify/bp/version-13-hotfix/pr-37069
fix: Recalculate `advance_paid` in SO/PO after unlinking from advance entry (backport #37069)
2023-09-23 11:05:10 +05:30
marination
d0c6f286cf test: Impact on SO of advance PE submit and unlinking/replacement by SI
(cherry picked from commit 8a4954d713)
2023-09-22 03:06:37 +00:00
marination
a6bef64c8e fix: Update advance_paid in SO/PO after unlinking from advance entry
(cherry picked from commit 426350eee6)
2023-09-22 03:06:36 +00:00
mergify[bot]
5092ea175e feat: Stock Ledger Variance report (backport #37165) (#37184)
feat: `Stock Ledger Variance` report (#37165)

* feat: `Stock Ledger Variance` report

* refactor: `get_data()`

(cherry picked from commit acda72d616)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-21 07:59:13 +00:00
mergify[bot]
5833c4dae2 fix: incorrect stock ledger entries in DN (backport #36944) (#37067)
* fix: incorrect stock ledger entries in DN (#36944)

(cherry picked from commit 0e83190c19)

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

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-09-20 18:12:08 +05:30
mergify[bot]
48eb6a6573 feat: provision to make reposting entries from Stock and Account Value Comparison Report (backport #35365) (#37171)
* feat: provision to make reposting entries from Stock and Account Value Comparison Report

(cherry picked from commit 7b818e9d34)

* fix: `linter`

* fix(ux): throw if no row selected to create repost entries

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-20 11:14:48 +00:00
Frappe PR Bot
7c1288f726 chore(release): Bumped to Version 13.53.0
# [13.53.0](https://github.com/frappe/erpnext/compare/v13.52.15...v13.53.0) (2023-09-19)

### Bug Fixes

* fetch logic for repay_from_salary in loan_repayment [v13] ([#37136](https://github.com/frappe/erpnext/issues/37136)) ([d4f8b05](d4f8b057e1))

### Features

* provision to create RIV from `Stock Ledger Invariant Check` report (backport [#37115](https://github.com/frappe/erpnext/issues/37115)) ([#37149](https://github.com/frappe/erpnext/issues/37149)) ([86b152f](86b152fe5c))
2023-09-19 13:22:41 +00:00
Deepesh Garg
c89715d6da Merge pull request #37154 from frappe/version-13-hotfix
chore: release v13
2023-09-19 18:50:07 +05:30
Anand Baburajan
d4f8b057e1 fix: fetch logic for repay_from_salary in loan_repayment [v13] (#37136)
fix: fetch logic for repay_from_salary in loan_repayment
2023-09-19 15:18:17 +05:30
mergify[bot]
86b152fe5c feat: provision to create RIV from Stock Ledger Invariant Check report (backport #37115) (#37149)
* feat: provision to create RIV from `Stock Ledger Invariant Check` report (#37115)

* feat: provision to create RIV from `Stock Ledger Invariant Check` report

* fix: `linter`

(cherry picked from commit 9c9d0ecb73)

# Conflicts:
#	erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-19 09:19:46 +00:00
Frappe PR Bot
11b16569e5 chore(release): Bumped to Version 13.52.15
## [13.52.15](https://github.com/frappe/erpnext/compare/v13.52.14...v13.52.15) (2023-09-12)

### Bug Fixes

* prorate factor for subscription plan ([#36953](https://github.com/frappe/erpnext/issues/36953)) ([9a15ed8](9a15ed8083))
2023-09-12 12:21:41 +00:00
Deepesh Garg
cd99630457 Merge pull request #37044 from frappe/version-13-hotfix
chore: release v13
2023-09-12 17:50:08 +05:30
Frappe PR Bot
efab1e7361 chore(release): Bumped to Version 13.52.14
## [13.52.14](https://github.com/frappe/erpnext/compare/v13.52.13...v13.52.14) (2023-09-10)

### Bug Fixes

* prorate factor for subscription plan ([#36953](https://github.com/frappe/erpnext/issues/36953)) ([d7f09f8](d7f09f8795))
2023-09-10 18:30:02 +00:00
Deepesh Garg
63a6e7e35c Merge pull request #37018 from frappe/mergify/bp/version-13/pr-37009
fix: prorate factor for subscription plan (#36953)
2023-09-10 23:58:35 +05:30
Gursheen Kaur Anand
d7f09f8795 fix: prorate factor for subscription plan (#36953)
(cherry picked from commit fc79af5926)
(cherry picked from commit 9a15ed8083)
2023-09-10 17:58:33 +00:00
Deepesh Garg
437a294621 Merge pull request #37009 from frappe/mergify/bp/version-13-hotfix/pr-36953
fix: prorate factor for subscription plan (#36953)
2023-09-10 12:21:09 +05:30
Gursheen Kaur Anand
9a15ed8083 fix: prorate factor for subscription plan (#36953)
(cherry picked from commit fc79af5926)
2023-09-10 06:06:12 +00:00
mergify[bot]
3a49d4f9b3 chore: asset finance books validation (backport #36979) (#36982)
* chore: asset finance books validation (#36979)

(cherry picked from commit 0077659e93)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py

* chore: fix conflicts

* chore: fix tests

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-09-06 15:39:44 +05:30
Frappe PR Bot
8b2c6ed61a chore(release): Bumped to Version 13.52.13
## [13.52.13](https://github.com/frappe/erpnext/compare/v13.52.12...v13.52.13) (2023-09-06)

### Bug Fixes

* ask for asset related accounts only when needed (backport [#36960](https://github.com/frappe/erpnext/issues/36960)) ([#36972](https://github.com/frappe/erpnext/issues/36972)) ([3a71fa9](3a71fa9d96))
* update repay_from_salary, payment_account and payroll_payable_account fields ([#36949](https://github.com/frappe/erpnext/issues/36949)) ([6f40d0c](6f40d0cdf6))
2023-09-06 07:24:52 +00:00
Deepesh Garg
4f98e958a1 Merge pull request #36962 from frappe/version-13-hotfix
chore: release v13
2023-09-06 12:52:49 +05:30
mergify[bot]
3a71fa9d96 fix: ask for asset related accounts only when needed (backport #36960) (#36972)
fix: ask for asset related accounts only when needed (#36960)

* fix: only ask for asset_received_but_not_billed account when needed

* chore: remove unnecessary if condition

* fix: only ask for expenses_included_in_asset_valuation account when needed

(cherry picked from commit 174f95d699)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-09-05 18:14:49 +05:30
Anand Baburajan
6f40d0cdf6 fix: update repay_from_salary, payment_account and payroll_payable_account fields (#36949)
* fix: update repay_from_salary, payment_account and payroll_payable_account fields

* chore: formatting
2023-09-05 12:43:09 +05:30
Anand Baburajan
5bd6c27b05 chore: patch to correct asset values if je has workflow [v13] (#36916)
chore: patch to correct asset values if je has workflow
2023-09-02 18:32:23 +05:30
Frappe PR Bot
9876019c69 chore(release): Bumped to Version 13.52.12
## [13.52.12](https://github.com/frappe/erpnext/compare/v13.52.11...v13.52.12) (2023-08-22)

### Bug Fixes

* disallow mulitple SO with same PO No ([3b9ac9f](3b9ac9f46a))
* timeout error coming during reposting (backport [#36715](https://github.com/frappe/erpnext/issues/36715)) ([#36753](https://github.com/frappe/erpnext/issues/36753)) ([783bb93](783bb93913))
2023-08-22 16:59:52 +00:00
Deepesh Garg
f4fb878282 Merge pull request #36762 from frappe/version-13-hotfix
chore: release v13
2023-08-22 22:28:20 +05:30
mergify[bot]
783bb93913 fix: timeout error coming during reposting (backport #36715) (#36753)
fix: timeout error coming during reposting (#36715)

(cherry picked from commit 620b21fec5)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-22 13:22:48 +05:30
Frappe PR Bot
cfbd9af100 chore(release): Bumped to Version 13.52.11
## [13.52.11](https://github.com/frappe/erpnext/compare/v13.52.10...v13.52.11) (2023-08-17)

### Bug Fixes

* disallow mulitple SO with same PO No ([bdaae81](bdaae81171))
2023-08-17 10:12:01 +00:00
ruthra kumar
966c296872 Merge pull request #36680 from frappe/mergify/bp/version-13/pr-36590
fix: disallow mulitple SO with same Purchase Order No if not enabled in Settings (backport #36590)
2023-08-17 15:40:34 +05:30
ruthra kumar
0ff871e38e chore: resolve conflict 2023-08-17 09:11:47 +05:30
ruthra kumar
9df10dbc40 refactor(test): don't set po_no by default
(cherry picked from commit 64614cd915)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2023-08-17 03:33:56 +00:00
ruthra kumar
bdaae81171 fix: disallow mulitple SO with same PO No
(cherry picked from commit dbd3fdbb41)
2023-08-17 03:33:56 +00:00
ruthra kumar
066cf0e3bc Merge pull request #36671 from frappe/mergify/bp/version-13-hotfix/pr-36590
fix: disallow mulitple SO with same Purchase Order No if not enabled in Settings (backport #36590)
2023-08-16 16:21:33 +05:30
ruthra kumar
829298066f chore: resolve conflict 2023-08-16 15:02:04 +05:30
ruthra kumar
006da22d3f refactor(test): don't set po_no by default
(cherry picked from commit 64614cd915)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2023-08-16 09:12:18 +00:00
ruthra kumar
3b9ac9f46a fix: disallow mulitple SO with same PO No
(cherry picked from commit dbd3fdbb41)
2023-08-16 09:12:17 +00:00
81 changed files with 1966 additions and 697 deletions

View File

@@ -11,10 +11,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: '3.10'
- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3
@@ -22,10 +22,8 @@ jobs:
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- uses: returntocorp/semgrep-action@v1
env:
SEMGREP_TIMEOUT: 120
with:
config: >-
r/python.lang.correctness
./frappe-semgrep-rules/rules
- name: Download semgrep
run: pip install semgrep
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness

View File

@@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = "13.52.10"
__version__ = "13.54.4"
def get_default_company(user=None):

View File

@@ -4,6 +4,12 @@
frappe.ui.form.on('Accounts Settings', {
refresh: function(frm) {
},
validate_access_key(frm) {
frappe.call({
doc: frm.doc,
method: "validate_access_key"
});
}
});

View File

@@ -46,6 +46,10 @@
"currency_exchange_section",
"allow_stale",
"stale_days",
"service_provider",
"column_break_eiyok",
"access_key",
"validate_access_key",
"report_settings_sb",
"use_custom_cash_flow"
],
@@ -282,32 +286,56 @@
"label": "Enable Common Party Accounting"
},
{
"default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
}
"default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
},
{
"default": "frankfurter.app",
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host"
},
{
"depends_on": "eval:doc.service_provider == \"exchangerate.host\"",
"description": "Access Key is mandatory for exchangerate.host",
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
},
{
"depends_on": "eval:doc.service_provider == \"exchangerate.host\"",
"fieldname": "validate_access_key",
"fieldtype": "Button",
"label": "Validate Access Key"
},
{
"fieldname": "column_break_eiyok",
"fieldtype": "Column Break"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-06-13 18:47:46.430291",
"modified": "2023-10-07 14:20:01.779208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -8,12 +8,43 @@ import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils import cint, nowdate
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
@frappe.whitelist()
def validate_access_key(self):
if self.service_provider == "exchangerate.host":
if not self.access_key:
frappe.throw(_("Access Key is required for exchangerate.host"))
else:
import requests
# Validate access key
api_url = "https://api.exchangerate.host/convert"
response = requests.get(
api_url,
params={
"access_key": self.access_key,
"transaction_date": nowdate(),
"amount": 1,
"from": "USD",
"to": "INR",
},
)
# exchangerate.host return 200 for all requests. Can't rely on it to raise exception
if not response.json()["success"]:
frappe.throw(
title=_("Service Provider Error"),
msg=_("Currency exchange rate serivce provider: {0} returned Error. {1}").format(
frappe.bold(self.service_provider), response.json()
),
exc=frappe.ValidationError,
)
frappe.msgprint(msg=_("Success"), title=_("Access Key Validation"))
def on_update(self):
frappe.clear_cache()

View File

@@ -358,6 +358,7 @@ def update_outstanding_amt(
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
bal = flt(bal, frappe.get_precision(against_voucher_type, "outstanding_amount"))
# Didn't use db_set for optimization purpose
ref_doc.outstanding_amount = bal
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)

View File

@@ -175,52 +175,53 @@ class PaymentEntry(AccountsController):
frappe.throw(fail_message.format(d.idx))
def validate_allocated_amount_with_latest_data(self):
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
"company": self.company,
"party_type": self.party_type,
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
}
)
if self.references:
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
"company": self.company,
"party_type": self.party_type,
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
}
)
# Group latest_references by (voucher_type, voucher_no)
latest_lookup = {}
for d in latest_references:
d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
# Group latest_references by (voucher_type, voucher_no)
latest_lookup = {}
for d in latest_references:
d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
for d in self.get("references"):
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
for d in self.get("references"):
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
).format(_(d.reference_doctype), d.reference_name)
)
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
).format(_(d.reference_doctype), d.reference_name)
)
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:

View File

@@ -295,6 +295,7 @@ class PaymentReconciliation(Document):
"amount": pay.get("amount"),
"allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"),
"currency": inv.get("currency"),
}
)

View File

@@ -20,7 +20,8 @@
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account"
"difference_account",
"currency"
],
"fields": [
{
@@ -37,7 +38,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "Currency",
"options": "currency",
"reqd": 1
},
{
@@ -112,7 +113,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@@ -120,7 +121,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@@ -129,11 +130,18 @@
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
}
],
"istable": 1,
"links": [],
"modified": "2021-10-06 11:48:59.616562",
"modified": "2023-11-28 16:30:43.344612",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -704,7 +704,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -715,7 +715,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()

View File

@@ -261,9 +261,7 @@ class PurchaseInvoice(BuyingController):
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
stock_items = self.get_stock_items()
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
if len(asset_items) > 0:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
asset_received_but_not_billed = None
if self.update_stock:
self.validate_item_code()
@@ -357,6 +355,8 @@ class PurchaseInvoice(BuyingController):
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -924,8 +924,9 @@ class PurchaseInvoice(BuyingController):
)
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
arbnb_account = None
eiiav_account = None
asset_eiiav_currency = None
for item in self.get("items"):
if item.is_fixed_asset:
@@ -937,6 +938,8 @@ class PurchaseInvoice(BuyingController):
"Asset Received But Not Billed",
"Fixed Asset",
]:
if not arbnb_account:
arbnb_account = self.get_company_default("asset_received_but_not_billed")
item.expense_account = arbnb_account
if not self.update_stock:
@@ -959,7 +962,10 @@ class PurchaseInvoice(BuyingController):
)
if item.item_tax_amount:
asset_eiiav_currency = get_account_currency(eiiav_account)
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
@@ -1002,7 +1008,10 @@ class PurchaseInvoice(BuyingController):
)
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
asset_eiiav_currency = get_account_currency(eiiav_account)
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
@@ -1022,47 +1031,46 @@ class PurchaseInvoice(BuyingController):
)
)
# When update stock is checked
# Assets are bought through this document then it will be linked to this document
if self.update_stock:
if flt(item.landed_cost_voucher_amount):
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
if flt(item.landed_cost_voucher_amount):
if not eiiav_account:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
return gl_entries

View File

@@ -1626,6 +1626,30 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
0,
)
def test_default_cost_center_for_purchase(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]:
create_cost_center(cost_center_name=c_center)
item = create_item(
"_Test Cost Center Item For Purchase",
is_stock_item=1,
buying_cost_center="_Test Cost Center Buying - _TC",
selling_cost_center="_Test Cost Center Selling - _TC",
)
pi = make_purchase_invoice(
item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center=""
)
pi.items[0].cost_center = ""
pi.set_missing_values()
pi.calculate_taxes_and_totals()
pi.save()
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -1783,6 +1783,10 @@ class TestSalesInvoice(unittest.TestCase):
)
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
"""Test impact of advance PE submission/cancellation on SI and SO."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -1802,10 +1806,25 @@ class TestSalesInvoice(unittest.TestCase):
"paid_to": "_Test Cash - _TC",
}
)
pe.append(
"references",
{
"reference_doctype": "Sales Order",
"reference_name": sales_order.name,
"total_amount": sales_order.grand_total,
"outstanding_amount": sales_order.grand_total,
"allocated_amount": 300,
},
)
pe.insert()
pe.submit()
sales_order.reload()
self.assertEqual(sales_order.advance_paid, 300)
si = frappe.copy_doc(test_records[0])
si.items[0].sales_order = sales_order.name
si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0
si.append(
"advances",
@@ -1813,6 +1832,7 @@ class TestSalesInvoice(unittest.TestCase):
"doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry",
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 300,
"allocated_amount": 300,
"remarks": pe.remarks,
@@ -1821,7 +1841,13 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
si.submit()
si.load_from_db()
si.reload()
pe.reload()
sales_order.reload()
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(sales_order.advance_paid, 0.0)
# check outstanding after advance allocation
self.assertEqual(
@@ -1829,11 +1855,9 @@ class TestSalesInvoice(unittest.TestCase):
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
)
# added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
si.reload()
si.load_from_db()
# check outstanding after advance cancellation
self.assertEqual(
flt(si.outstanding_amount),
@@ -2448,36 +2472,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_sle_for_target_warehouse(self):
se = make_stock_entry(
item_code="138-CMS Shoe",
target="Finished Goods - _TC",
company="_Test Company",
qty=1,
basic_rate=500,
)
si = frappe.copy_doc(test_records[0])
si.update_stock = 1
si.set_warehouse = "Finished Goods - _TC"
si.set_target_warehouse = "Stores - _TC"
si.get("items")[0].warehouse = "Finished Goods - _TC"
si.get("items")[0].target_warehouse = "Stores - _TC"
si.insert()
si.submit()
sles = frappe.get_all(
"Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"]
)
# check if both SLEs are created
self.assertEqual(len(sles), 2)
self.assertEqual(sum(d.actual_qty for d in sles), 0.0)
# tear down
si.cancel()
se.cancel()
def test_internal_transfer_gl_entry(self):
si = create_sales_invoice(
company="_Test Company with perpetual inventory",

View File

@@ -694,3 +694,23 @@ class TestSubscription(unittest.TestCase):
# Check the currency of the created invoice
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
self.assertEqual(currency, "USD")
def test_plan_rate_for_midmonth_start_date(self):
subscription = frappe.new_doc("Subscription")
subscription.party_type = "Supplier"
subscription.party = "_Test Supplier"
subscription.generate_invoice_at_period_start = 1
subscription.follow_calendar_months = 1
subscription.generate_new_invoices_past_due_date = 1
subscription.start_date = "2023-04-08"
subscription.end_date = "2024-02-27"
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
pi = frappe.get_doc("Purchase Invoice", subscription.invoices[0].invoice)
self.assertEqual(pi.total, 55333.33)
subscription.delete()

View File

@@ -56,18 +56,17 @@ def get_plan_rate(
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
if prorate:
prorate_factor = flt(
date_diff(start_date, get_first_day(start_date))
/ date_diff(get_last_day(start_date), get_first_day(start_date)),
1,
)
prorate_factor += flt(
date_diff(get_last_day(end_date), end_date)
/ date_diff(get_last_day(end_date), get_first_day(end_date)),
1,
)
cost -= plan.cost * prorate_factor
cost -= plan.cost * get_prorate_factor(start_date, end_date)
return cost
def get_prorate_factor(start_date, end_date):
total_days_to_skip = date_diff(start_date, get_first_day(start_date))
total_days_in_month = int(get_last_day(start_date).strftime("%d"))
prorate_factor = flt(total_days_to_skip / total_days_in_month)
total_days_to_skip = date_diff(get_last_day(end_date), end_date)
total_days_in_month = int(get_last_day(end_date).strftime("%d"))
prorate_factor += flt(total_days_to_skip / total_days_in_month)
return prorate_factor

View File

@@ -4,12 +4,7 @@
import frappe
from frappe import _, msgprint, scrub
from frappe.contacts.doctype.address.address import (
get_address_display,
get_company_address,
get_default_address,
)
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.utils import (
@@ -120,6 +115,7 @@ def _get_party_details(
party_address,
company_address,
shipping_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
set_other_values(party_details, party, party_type)
@@ -183,6 +179,8 @@ def set_address_details(
party_address=None,
company_address=None,
shipping_address=None,
*,
ignore_permissions=False
):
billing_address_field = (
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
@@ -195,13 +193,17 @@ def set_address_details(
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
)
# address display
party_details.address_display = get_address_display(party_details[billing_address_field])
party_details.address_display = render_address(
party_details[billing_address_field], check_permissions=not ignore_permissions
)
# shipping address
if party_type in ["Customer", "Lead"]:
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
party_type, party.name
)
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
party_details.shipping_address = render_address(
party_details["shipping_address_name"], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
@@ -224,7 +226,9 @@ def set_address_details(
party_details.update(
{
"shipping_address": shipping_address,
"shipping_address_display": get_address_display(shipping_address),
"shipping_address_display": render_address(
shipping_address, check_permissions=not ignore_permissions
),
**get_fetch_values(doctype, "shipping_address", shipping_address),
}
)
@@ -235,7 +239,8 @@ def set_address_details(
{
"billing_address": party_details.company_address,
"billing_address_display": (
party_details.company_address_display or get_address_display(party_details.company_address)
party_details.company_address_display
or render_address(party_details.company_address, check_permissions=True)
),
**get_fetch_values(doctype, "billing_address", party_details.company_address),
}
@@ -277,7 +282,34 @@ def set_contact_details(party_details, party, party_type):
}
)
else:
party_details.update(get_contact_details(party_details.contact_person))
fields = [
"name as contact_person",
"salutation",
"first_name",
"last_name",
"email_id as contact_email",
"mobile_no as contact_mobile",
"phone as contact_phone",
"designation as contact_designation",
"department as contact_department",
]
contact_details = frappe.db.get_value(
"Contact", party_details.contact_person, fields, as_dict=True
)
contact_details.contact_display = " ".join(
filter(
None,
[
contact_details.get("salutation"),
contact_details.get("first_name"),
contact_details.get("last_name"),
],
)
)
party_details.update(contact_details)
def set_other_values(party_details, party, party_type):
@@ -938,3 +970,13 @@ def add_party_account(party_type, party, company, account):
doc.append("accounts", accounts)
doc.save()
def render_address(address, check_permissions=True):
try:
from frappe.contacts.doctype.address.address import render_address as _render
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render
return frappe.call(_render, address, check_permissions=check_permissions)

View File

@@ -400,12 +400,20 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters
for entries in gl_entries_by_account.values():
for entry in entries:
if entry.account_number:
<<<<<<< HEAD
account_name = entry.account_number + " - " + entry.account_name
else:
account_name = entry.account_name
d = accounts_by_name.get(account_name)
=======
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
d = accounts_by_name.get(account_name)
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
if d:
debit, credit = 0, 0
for company in companies:
@@ -482,9 +490,15 @@ def update_parent_account_names(accounts):
for d in accounts:
if d.account_number:
<<<<<<< HEAD
account_name = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
=======
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
name_to_account_map[d.name] = account_name
for account in accounts:
@@ -660,9 +674,15 @@ def set_gl_entries_by_account(
for entry in gl_entries:
if entry.account_number:
<<<<<<< HEAD
account_name = entry.account_number + " - " + entry.account_name
else:
account_name = entry.account_name
=======
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(account_name, []).append(entry)
@@ -770,10 +790,16 @@ def filter_accounts(accounts, depth=10):
accounts_by_name = {}
for d in accounts:
if d.account_number:
<<<<<<< HEAD
account_name = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
d["company_wise_opening_bal"] = defaultdict(float)
=======
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)

View File

@@ -457,6 +457,8 @@ class GrossProfitGenerator(object):
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
if self.filters.get("group_by") == "Sales Person":
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
else:

View File

@@ -537,6 +537,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -596,6 +600,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()
original_row = existing_row.as_dict().copy()
existing_row.update(reference_details)

View File

@@ -41,6 +41,7 @@ class Asset(AccountsController):
self.validate_item()
self.validate_cost_center()
self.set_missing_values()
self.validate_finance_books()
self.prepare_depreciation_data()
self.validate_gross_and_purchase_amount()
if self.get("schedules"):
@@ -197,6 +198,27 @@ class Asset(AccountsController):
finance_books = get_item_details(self.item_code, self.asset_category)
self.set("finance_books", finance_books)
def validate_finance_books(self):
if not self.calculate_depreciation or len(self.finance_books) == 1:
return
finance_books = set()
for d in self.finance_books:
if d.finance_book in finance_books:
frappe.throw(
_("Row #{}: Please use a different Finance Book.").format(d.idx),
title=_("Duplicate Finance Book"),
)
else:
finance_books.add(d.finance_book)
if not d.finance_book:
frappe.throw(
_("Row #{}: Finance Book should not be empty since you're using multiple.").format(d.idx),
title=_("Missing Finance Book"),
)
def validate_asset_values(self):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")

View File

@@ -1287,6 +1287,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
@@ -1297,6 +1298,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
@@ -1307,6 +1309,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 3",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1336,6 +1339,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1346,6 +1350,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
@@ -1581,6 +1586,15 @@ def create_asset_data():
if not frappe.db.exists("Location", "Test Location"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 1"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 2"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 3"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert()
def create_asset(**args):
args = frappe._dict(args)

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display
from frappe.contacts.doctype.address.address import render_address
from frappe.utils import cint, cstr, flt, getdate
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@@ -14,7 +14,8 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class QtyMismatchError(ValidationError):
@@ -186,7 +187,9 @@ class BuyingController(StockController, Subcontracting):
for address_field, address_display_field in address_dict.items():
if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field)))
self.set(
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
def set_total_in_words(self):
from frappe.utils import money_in_words
@@ -504,9 +507,20 @@ class BuyingController(StockController, Subcontracting):
)
if self.is_return:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
if get_valuation_method(d.item_code) == "Moving Average":
previous_sle = get_previous_sle(
{
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
outgoing_rate = flt(previous_sle.get("valuation_rate"))
else:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
if d.from_warehouse:

View File

@@ -4,9 +4,9 @@
import frappe
from frappe import _, bold, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
from erpnext.accounts.party import render_address
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
@@ -583,7 +583,9 @@ class SellingController(StockController):
for address_field, address_display_field in address_dict.items():
if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field)))
self.set(
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
def validate_for_duplicate_items(self):
check_list, chk_dupl_itm = [], []

View File

@@ -582,13 +582,21 @@ class StockController(AccountsController):
d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self):
if (
self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt")
and self.is_internal_transfer()
):
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"):
if self.is_internal_transfer():
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
else:
self.validate_internal_transfer_warehouse()
def validate_internal_transfer_warehouse(self):
for row in self.items:
if row.get("target_warehouse"):
row.target_warehouse = None
if row.get("from_warehouse"):
row.from_warehouse = None
def validate_in_transit_warehouses(self):
if (
@@ -906,8 +914,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = "Item and Warehouse"
repost_entry.voucher_type = voucher_type
repost_entry.voucher_no = voucher_no
repost_entry.item_code = sle.item_code
repost_entry.warehouse = sle.warehouse

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Concat_ws, Date
def execute(filters=None):
@@ -69,53 +70,41 @@ def get_columns():
def get_data(filters):
return frappe.db.sql(
"""
SELECT
`tabLead`.name,
`tabLead`.lead_name,
`tabLead`.status,
`tabLead`.lead_owner,
`tabLead`.territory,
`tabLead`.source,
`tabLead`.email_id,
`tabLead`.mobile_no,
`tabLead`.phone,
`tabLead`.owner,
`tabLead`.company,
concat_ws(', ',
trim(',' from `tabAddress`.address_line1),
trim(',' from tabAddress.address_line2)
) AS address,
`tabAddress`.state,
`tabAddress`.pincode,
`tabAddress`.country
FROM
`tabLead` left join `tabDynamic Link` on (
`tabLead`.name = `tabDynamic Link`.link_name and
`tabDynamic Link`.parenttype = 'Address')
left join `tabAddress` on (
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(
conditions=get_conditions(filters)
),
filters,
as_dict=1,
lead = frappe.qb.DocType("Lead")
address = frappe.qb.DocType("Address")
dynamic_link = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(lead)
.left_join(dynamic_link)
.on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address"))
.left_join(address)
.on(address.name == dynamic_link.parent)
.select(
lead.name,
lead.lead_name,
lead.status,
lead.lead_owner,
lead.territory,
lead.source,
lead.email_id,
lead.mobile_no,
lead.phone,
lead.owner,
lead.company,
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
address.state,
address.pincode,
address.country,
)
.where(lead.company == filters.company)
.where(Date(lead.creation).between(filters.from_date, filters.to_date))
)
def get_conditions(filters):
conditions = []
if filters.get("territory"):
conditions.append(" and `tabLead`.territory=%(territory)s")
query = query.where(lead.territory == filters.get("territory"))
if filters.get("status"):
conditions.append(" and `tabLead`.status=%(status)s")
query = query.where(lead.status == filters.get("status"))
return " ".join(conditions) if conditions else ""
return query.run(as_dict=1)

View File

@@ -17,7 +17,6 @@ from erpnext.e_commerce.shopping_cart.cart import (
request_for_quotation,
update_cart,
)
from erpnext.tests.utils import create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase):
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
@@ -46,48 +44,57 @@ class TestShoppingCart(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_get_cart_new_user(self):
self.login_as_new_user()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
# test if lead is created and quotation with new lead is fetched
quotation = _get_cart_quotation()
customer = frappe.get_doc("Customer", "_Test Customer 2")
quotation = _get_cart_quotation(party=customer)
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(
quotation.contact_person,
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
def test_get_cart_customer(self):
def validate_quotation():
def test_get_cart_customer(self, customer="_Test Customer 2"):
def validate_quotation(customer_name):
# test if quotation with customer is fetched
quotation = _get_cart_quotation()
party = frappe.get_doc("Customer", customer_name)
quotation = _get_cart_quotation(party=party)
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, "_Test Customer")
self.assertEqual(quotation.party_name, customer_name)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
validate_quotation()
self.login_as_customer()
quotation = validate_quotation()
quotation = validate_quotation(customer)
return quotation
def test_add_to_cart(self):
self.login_as_customer()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
# clear existing quotations
self.clear_existing_quotations()
# add first item
update_cart("_Test Item", 1)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -95,7 +102,7 @@ class TestShoppingCart(unittest.TestCase):
# add second item
update_cart("_Test Item 2", 1)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[1].qty, 1)
self.assertEqual(quotation.get("items")[1].amount, 20)
@@ -108,7 +115,7 @@ class TestShoppingCart(unittest.TestCase):
# update first item
update_cart("_Test Item", 5)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 5)
self.assertEqual(quotation.get("items")[0].amount, 50)
@@ -121,7 +128,7 @@ class TestShoppingCart(unittest.TestCase):
# remove first item
update_cart("_Test Item", 0)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -129,9 +136,20 @@ class TestShoppingCart(unittest.TestCase):
self.assertEqual(quotation.net_total, 20)
self.assertEqual(len(quotation.get("items")), 1)
@unittest.skip("Flaky in CI")
def test_tax_rule(self):
self.create_tax_rule()
self.login_as_customer()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
quotation = self.create_quotation()
from erpnext.accounts.party import set_taxes
@@ -319,7 +337,7 @@ class TestShoppingCart(unittest.TestCase):
if frappe.db.exists("User", email):
return
frappe.get_doc(
user = frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
@@ -329,6 +347,40 @@ class TestShoppingCart(unittest.TestCase):
}
).insert(ignore_permissions=True)
user.add_roles("Customer")
def create_address_and_contact(**kwargs):
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
frappe.get_doc(
{
"doctype": "Address",
"address_title": kwargs.get("address_title"),
"address_type": kwargs.get("address_type") or "Office",
"address_line1": kwargs.get("address_line1") or "Station Road",
"city": kwargs.get("city") or "_Test City",
"state": kwargs.get("state") or "Test State",
"country": kwargs.get("country") or "India",
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
).insert()
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": kwargs.get("first_name"),
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
)
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
contact.insert()
test_dependencies = [
"Sales Taxes and Charges Template",

View File

@@ -296,18 +296,27 @@ class LeaveAllocation(Document):
def get_previous_allocation(from_date, leave_type, employee):
"""Returns document properties of previous allocation"""
return frappe.db.get_value(
"Leave Allocation",
filters={
"to_date": ("<", from_date),
"leave_type": leave_type,
"employee": employee,
"docstatus": 1,
},
order_by="to_date DESC",
fieldname=["name", "from_date", "to_date", "employee", "leave_type"],
as_dict=1,
)
Allocation = frappe.qb.DocType("Leave Allocation")
allocations = (
frappe.qb.from_(Allocation)
.select(
Allocation.name,
Allocation.from_date,
Allocation.to_date,
Allocation.employee,
Allocation.leave_type,
)
.where(
(Allocation.employee == employee)
& (Allocation.leave_type == leave_type)
& (Allocation.to_date < from_date)
& (Allocation.docstatus == 1)
)
.orderby(Allocation.to_date, order=frappe.qb.desc)
.limit(1)
).run(as_dict=True)
return allocations[0] if allocations else None
def get_leave_allocation_for_period(

View File

@@ -706,19 +706,22 @@ def get_allocation_expiry_for_cf_leaves(
employee: str, leave_type: str, to_date: str, from_date: str
) -> str:
"""Returns expiry of carry forward allocation in leave ledger entry"""
expiry = frappe.get_all(
"Leave Ledger Entry",
filters={
"employee": employee,
"leave_type": leave_type,
"is_carry_forward": 1,
"transaction_type": "Leave Allocation",
"to_date": ["between", (from_date, to_date)],
"docstatus": 1,
},
fields=["to_date"],
)
return expiry[0]["to_date"] if expiry else ""
Ledger = frappe.qb.DocType("Leave Ledger Entry")
expiry = (
frappe.qb.from_(Ledger)
.select(Ledger.to_date)
.where(
(Ledger.employee == employee)
& (Ledger.leave_type == leave_type)
& (Ledger.is_carry_forward == 1)
& (Ledger.transaction_type == "Leave Allocation")
& (Ledger.to_date.between(from_date, to_date))
& (Ledger.docstatus == 1)
)
.limit(1)
).run()
return expiry[0][0] if expiry else ""
@frappe.whitelist()
@@ -1017,7 +1020,7 @@ def get_leaves_for_period(
if leave_entry.leaves % 1:
half_day = 1
half_day_date = frappe.db.get_value(
"Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"]
"Leave Application", leave_entry.transaction_name, "half_day_date"
)
leave_days += (

View File

@@ -713,25 +713,31 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(details.leave_balance, 30)
def test_earned_leaves_creation(self):
from erpnext.hr.utils import allocate_earned_leaves
from erpnext.hr.doctype.leave_policy_assignment.test_leave_policy_assignment import (
allocate_earned_leaves_for_months,
)
leave_period = get_leave_period()
year_start = get_year_start(getdate())
year_end = get_year_ending(getdate())
frappe.flags.current_date = year_start
leave_period = get_leave_period(year_start, year_end)
employee = get_employee()
leave_type = "Test Earned Leave Type"
make_policy_assignment(employee, leave_type, leave_period)
for i in range(0, 14):
allocate_earned_leaves()
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
# leaves for 6 months = 3, but max leaves restricts allocation to 2
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 2)
allocate_earned_leaves_for_months(6)
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 2)
# validate earned leaves creation without maximum leaves
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
allocate_earned_leaves_for_months(5)
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 4.5)
for i in range(0, 6):
allocate_earned_leaves()
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
frappe.flags.current_date = None
# test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self):
@@ -1254,7 +1260,7 @@ def set_leave_approver():
dept_doc.save(ignore_permissions=True)
def get_leave_period():
def get_leave_period(from_date=None, to_date=None):
leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"})
if leave_period_name:
return frappe.get_doc("Leave Period", leave_period_name[0][0])
@@ -1263,8 +1269,8 @@ def get_leave_period():
dict(
name="Test Leave Period",
doctype="Leave Period",
from_date=add_months(nowdate(), -6),
to_date=add_months(nowdate(), 6),
from_date=from_date or add_months(nowdate(), -6),
to_date=to_date or add_months(nowdate(), 6),
company="_Test Company",
is_active=1,
)

View File

@@ -27,7 +27,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
"options": "Employee",
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
@@ -57,13 +58,15 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Transaction Type",
"options": "DocType"
"options": "DocType",
"search_index": 1
},
{
"fieldname": "transaction_name",
"fieldtype": "Dynamic Link",
"label": "Transaction Name",
"options": "transaction_type"
"options": "transaction_type",
"search_index": 1
},
{
"fieldname": "leaves",
@@ -123,7 +126,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-01-04 18:47:45.146652",
"modified": "2023-11-17 12:36:36.963697",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Ledger Entry",

View File

@@ -225,3 +225,7 @@ def expire_carried_forward_allocation(allocation):
to_date=allocation.to_date,
)
create_leave_ledger_entry(allocation, args)
def on_doctype_update():
frappe.db.add_index("Leave Ledger Entry", ["transaction_type", "transaction_name"])

View File

@@ -100,7 +100,7 @@ class LeavePolicyAssignment(Document):
return leave_allocations
def create_leave_allocation(
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
self, leave_type, annual_allocation, leave_type_details, date_of_joining
):
# Creates leave allocation for the given employee in the provided leave period
carry_forward = self.carry_forward
@@ -108,7 +108,7 @@ class LeavePolicyAssignment(Document):
carry_forward = 0
new_leaves_allocated = self.get_new_leaves(
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
leave_type, annual_allocation, leave_type_details, date_of_joining
)
allocation = frappe.get_doc(
@@ -129,7 +129,7 @@ class LeavePolicyAssignment(Document):
allocation.submit()
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
def get_new_leaves(self, leave_type, annual_allocation, leave_type_details, date_of_joining):
from frappe.model.meta import get_field_precision
precision = get_field_precision(
@@ -146,20 +146,27 @@ class LeavePolicyAssignment(Document):
else:
# get leaves for past months if assignment is based on Leave Period / Joining Date
new_leaves_allocated = self.get_leaves_for_passed_months(
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
leave_type, annual_allocation, leave_type_details, date_of_joining
)
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
elif getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
date_diff(self.effective_to, self.effective_from) + 1
)
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
else:
if getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
date_diff(self.effective_to, self.effective_from) + 1
)
new_leaves_allocated = ceil(annual_allocation * remaining_period)
else:
new_leaves_allocated = annual_allocation
# leave allocation should not exceed annual allocation as per policy assignment
if new_leaves_allocated > annual_allocation:
new_leaves_allocated = annual_allocation
return flt(new_leaves_allocated, precision)
def get_leaves_for_passed_months(
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
self, leave_type, annual_allocation, leave_type_details, date_of_joining
):
from erpnext.hr.utils import get_monthly_earned_leave
@@ -184,7 +191,7 @@ class LeavePolicyAssignment(Document):
if months_passed > 0:
monthly_earned_leave = get_monthly_earned_leave(
new_leaves_allocated,
annual_allocation,
leave_type_details.get(leave_type).earned_leave_frequency,
leave_type_details.get(leave_type).rounding,
)

View File

@@ -5,8 +5,10 @@ import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
from frappe.utils import add_days, add_months, get_first_day, get_last_day, get_year_start, getdate
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
get_leave_period,
@@ -15,6 +17,7 @@ from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_polic
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
from erpnext.hr.utils import allocate_earned_leaves
test_dependencies = ["Employee"]
@@ -34,6 +37,8 @@ class TestLeavePolicyAssignment(FrappeTestCase):
self.original_doj = employee.date_of_joining
self.employee = employee
self.leave_type = "Test Earned Leave"
def test_grant_leaves(self):
leave_period = get_leave_period()
# allocation = 10
@@ -326,6 +331,90 @@ class TestLeavePolicyAssignment(FrappeTestCase):
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
def test_overallocation(self):
"""Tests if earned leave allocation does not exceed annual allocation"""
frappe.flags.current_date = get_year_start(getdate())
make_policy_assignment(
self.employee,
annual_allocation=22,
allocate_on_day="First Day",
start_date=frappe.flags.current_date,
)
# leaves for 12 months = 22
# With rounding, 22 leaves would be allocated in 11 months only
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
allocate_earned_leaves_for_months(11)
self.assertEqual(
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
)
# should not allocate more leaves than annual allocation
allocate_earned_leaves_for_months(1)
self.assertEqual(
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
)
def test_over_allocation_during_assignment_creation(self):
"""Tests backdated earned leave allocation does not exceed annual allocation"""
start_date = get_first_day(add_months(getdate(), -12))
# joining date set to 1Y ago
self.employee.date_of_joining = start_date
self.employee.save()
# create backdated assignment for last year
frappe.flags.current_date = get_first_day(getdate())
leave_policy_assignments = make_policy_assignment(
self.employee, start_date=start_date, allocate_on_day="Date of Joining"
)
# 13 months have passed but annual allocation = 12
# check annual allocation is not exceeded
leaves_allocated = get_allocated_leaves(leave_policy_assignments[0])
self.assertEqual(leaves_allocated, 12)
def test_overallocation_with_carry_forwarding(self):
"""Tests earned leave allocation with cf leaves does not exceed annual allocation"""
year_start = get_year_start(getdate())
# initial leave allocation = 5
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type=self.leave_type,
from_date=get_first_day(add_months(year_start, -1)),
to_date=get_last_day(add_months(year_start, -1)),
new_leaves_allocated=5,
carry_forward=0,
)
leave_allocation.submit()
frappe.flags.current_date = year_start
# carry forwarded leaves = 5
make_policy_assignment(
self.employee,
annual_allocation=22,
allocate_on_day="First Day",
start_date=year_start,
carry_forward=True,
)
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
allocate_earned_leaves_for_months(11)
# 5 carry forwarded leaves + 22 EL allocated = 27 leaves
self.assertEqual(
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
)
# should not allocate more leaves than annual allocation (22 excluding 5 cf leaves)
allocate_earned_leaves_for_months(1)
self.assertEqual(
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
)
def tearDown(self):
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
frappe.flags.current_date = None
@@ -376,3 +465,51 @@ def setup_leave_period_and_policy(start_date, based_on_doj=False):
).insert()
return leave_period, leave_policy
def make_policy_assignment(
employee,
allocate_on_day="Last Day",
earned_leave_frequency="Monthly",
start_date=None,
annual_allocation=12,
carry_forward=0,
assignment_based_on="Leave Period",
):
leave_type = create_earned_leave_type("Test Earned Leave", allocate_on_day)
leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date)
leave_policy = frappe.get_doc(
{
"doctype": "Leave Policy",
"title": "Test Earned Leave Policy",
"leave_policy_details": [
{"leave_type": leave_type.name, "annual_allocation": annual_allocation}
],
}
).insert()
data = {
"assignment_based_on": assignment_based_on,
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
"carry_forward": carry_forward,
}
leave_policy_assignments = create_assignment_for_multiple_employees(
[employee.name], frappe._dict(data)
)
return leave_policy_assignments
def get_allocated_leaves(assignment):
return frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": assignment},
"total_leaves_allocated",
)
def allocate_earned_leaves_for_months(months):
for i in range(0, months):
frappe.flags.current_date = add_months(frappe.flags.current_date, 1)
allocate_earned_leaves()

View File

@@ -1,26 +1,32 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-02-22 15:29:34",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:18:04.317397",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Leave Balance",
"owner": "Administrator",
"ref_doctype": "Employee",
"report_name": "Employee Leave Balance",
"report_type": "Script Report",
"add_total_row": 0,
"columns": [],
"creation": "2013-02-22 15:29:34",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-11-17 13:28:40.669200",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Leave Balance",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Employee",
"report_name": "Employee Leave Balance",
"report_type": "Script Report",
"roles": [
{
"role": "HR User"
},
},
{
"role": "HR Manager"
},
{
"role": "Employee"
}
]
}

View File

@@ -85,19 +85,10 @@ def get_columns() -> List[Dict]:
def get_data(filters: Filters) -> List:
leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name")
conditions = get_conditions(filters)
leave_types = get_leave_types()
active_employees = get_employees(filters)
user = frappe.session.user
department_approver_map = get_department_leave_approver_map(filters.department)
active_employees = frappe.get_list(
"Employee",
filters=conditions,
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
consolidate_leave_types = len(active_employees) > 1 and filters.consolidate_leave_types
row = None
@@ -110,10 +101,6 @@ def get_data(filters: Filters) -> List:
row = frappe._dict({"leave_type": leave_type})
for employee in active_employees:
leave_approvers = department_approver_map.get(employee.department_name, []).append(
employee.leave_approver
)
if consolidate_leave_types:
row = frappe._dict()
else:
@@ -144,6 +131,35 @@ def get_data(filters: Filters) -> List:
return data
def get_leave_types() -> List[str]:
LeaveType = frappe.qb.DocType("Leave Type")
leave_types = (frappe.qb.from_(LeaveType).select(LeaveType.name).orderby(LeaveType.name)).run(
as_dict=True
)
return [leave_type.name for leave_type in leave_types]
def get_employees(filters: Filters) -> List[Dict]:
Employee = frappe.qb.DocType("Employee")
query = frappe.qb.from_(Employee).select(
Employee.name,
Employee.employee_name,
Employee.department,
)
for field in ["company", "department"]:
if filters.get(field):
query = query.where((getattr(Employee, field) == filters.get(field)))
if filters.get("employee"):
query = query.where(Employee.name == filters.get("employee"))
if filters.get("employee_status"):
query = query.where(Employee.status == filters.get("employee_status"))
return query.run(as_dict=True)
def get_opening_balance(
employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float
) -> float:
@@ -168,48 +184,6 @@ def get_opening_balance(
return opening_balance
def get_conditions(filters: Filters) -> Dict:
conditions = {}
if filters.employee:
conditions["name"] = filters.employee
if filters.company:
conditions["company"] = filters.company
if filters.department:
conditions["department"] = filters.department
if filters.employee_status:
conditions["status"] = filters.employee_status
return conditions
def get_department_leave_approver_map(department: Optional[str] = None):
# get current department and all its child
department_list = frappe.get_list(
"Department",
filters={"disabled": 0},
or_filters={"name": department, "parent_department": department},
pluck="name",
)
# retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all(
"Department Approver",
filters={"parentfield": "leave_approvers", "parent": ("in", department_list)},
fields=["parent", "approver"],
as_list=True,
)
approvers = {}
for k, v in approver_list:
approvers.setdefault(k, []).append(v)
return approvers
def get_allocated_and_expired_leaves(
from_date: str, to_date: str, employee: str, leave_type: str
) -> Tuple[float, float, float]:
@@ -244,7 +218,7 @@ def get_leave_ledger_entries(
from_date: str, to_date: str, employee: str, leave_type: str
) -> List[Dict]:
ledger = frappe.qb.DocType("Leave Ledger Entry")
records = (
return (
frappe.qb.from_(ledger)
.select(
ledger.employee,
@@ -270,8 +244,6 @@ def get_leave_ledger_entries(
)
).run(as_dict=True)
return records
def get_chart_data(data: List, filters: Filters) -> Dict:
labels = []

View File

@@ -6,9 +6,6 @@ import frappe
from frappe import _
from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import (
get_department_leave_approver_map,
)
def execute(filters=None):
@@ -54,17 +51,11 @@ def get_data(filters, leave_types):
active_employees = frappe.get_all(
"Employee",
filters=conditions,
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
fields=["name", "employee_name", "department", "user_id"],
)
department_approver_map = get_department_leave_approver_map(filters.get("department"))
data = []
for employee in active_employees:
leave_approvers = department_approver_map.get(employee.department_name, [])
if employee.leave_approver:
leave_approvers.append(employee.leave_approver)
row = [employee.name, employee.employee_name, employee.department]
available_leave = get_leave_details(employee.name, filters.date)
for leave_type in leave_types:

View File

@@ -459,7 +459,7 @@ def generate_leave_encashment():
def allocate_earned_leaves():
"""Allocate earned leaves to Employees"""
e_leave_types = get_earned_leaves()
today = getdate()
today = frappe.flags.current_date or getdate()
for e_leave_type in e_leave_types:
@@ -496,18 +496,28 @@ def allocate_earned_leaves():
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
allocation = frappe.get_doc("Leave Allocation", allocation.name)
annual_allocation = flt(annual_allocation, allocation.precision("total_leaves_allocated"))
earned_leaves = get_monthly_earned_leave(
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
)
allocation = frappe.get_doc("Leave Allocation", allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
new_allocation_without_cf = flt(
flt(allocation.get_existing_leave_count()) + flt(earned_leaves),
allocation.precision("total_leaves_allocated"),
)
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if new_allocation != allocation.total_leaves_allocated:
today_date = today()
if (
new_allocation != allocation.total_leaves_allocated
# annual allocation as per policy should not be exceeded
and new_allocation_without_cf <= annual_allocation
):
today_date = frappe.flags.current_date or getdate()
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)

View File

@@ -306,7 +306,7 @@ def get_last_accrual_date(loan, posting_date):
def get_last_disbursement_date(loan, posting_date):
last_disbursement_date = frappe.db.get_value(
"Loan Disbursement",
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
{"docstatus": 1, "against_loan": loan, "posting_date": ("<=", posting_date)},
"MAX(posting_date)",
)

View File

@@ -6,7 +6,12 @@
frappe.ui.form.on('Loan Repayment', {
// refresh: function(frm) {
// }
// },
setup: function(frm) {
frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary");
},
onload: function(frm) {
frm.set_query('against_loan', function() {
return {

View File

@@ -15,7 +15,6 @@
"posting_date",
"clearance_date",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
"repay_from_salary",
"payment_details_section",
@@ -41,6 +40,7 @@
"amended_from",
"accounting_details_section",
"payment_account",
"payroll_payable_account",
"penalty_income_account",
"column_break_36",
"loan_account"
@@ -262,7 +262,6 @@
},
{
"default": "0",
"fetch_from": "against_loan.repay_from_salary",
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
@@ -280,6 +279,7 @@
"label": "Accounting Details"
},
{
"depends_on": "eval:!doc.repay_from_salary",
"fetch_from": "against_loan.payment_account",
"fetch_if_empty": 1,
"fieldname": "payment_account",
@@ -311,11 +311,10 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-08-04 17:13:51.964203",
"modified": "2023-09-18 16:50:32.897005",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -351,6 +350,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -81,6 +81,11 @@ class LoanRepayment(AccountsController):
if amounts.get("due_date"):
self.due_date = amounts.get("due_date")
if self.repay_from_salary and not self.payroll_payable_account:
frappe.throw(_("Please set Payroll Payable Account in Loan Repayment"))
elif not self.repay_from_salary and self.payroll_payable_account:
self.repay_from_salary = 1
def check_future_entries(self):
future_repayment_date = frappe.db.get_value(
"Loan Repayment",

View File

@@ -377,3 +377,5 @@ execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_s
erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v13_0.update_docs_link
erpnext.patches.v13_0.correct_asset_value_if_je_with_workflow
execute:frappe.db.set_value("Accounts Settings", "Accounts Settings", "service_provider", "frankfurter.app")

View File

@@ -3,23 +3,27 @@ import frappe
def execute():
frappe.reload_doc("stock", "doctype", "quality_inspection_parameter")
params = set()
# get all distinct parameters from QI readigs table
reading_params = frappe.db.get_all(
"Quality Inspection Reading", fields=["distinct specification"]
)
reading_params = [d.specification for d in reading_params]
# get all parameters from QI readings table
for (p,) in frappe.db.get_all(
"Quality Inspection Reading", fields=["specification"], as_list=True
):
params.add(p.strip())
# get all distinct parameters from QI Template as some may be unused in QI
template_params = frappe.db.get_all(
"Item Quality Inspection Parameter", fields=["distinct specification"]
)
template_params = [d.specification for d in template_params]
# get all parameters from QI Template as some may be unused in QI
for (p,) in frappe.db.get_all(
"Item Quality Inspection Parameter", fields=["specification"], as_list=True
):
params.add(p.strip())
params = list(set(reading_params + template_params))
# because db primary keys are case insensitive, so duplicates will cause an exception
params = set({x.casefold(): x for x in params}.values())
for parameter in params:
if not frappe.db.exists("Quality Inspection Parameter", parameter):
frappe.get_doc(
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
).insert(ignore_permissions=True)
if frappe.db.exists("Quality Inspection Parameter", parameter):
continue
frappe.get_doc(
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
).insert(ignore_permissions=True)

View File

@@ -0,0 +1,119 @@
import frappe
from frappe.model.workflow import get_workflow_name
from frappe.query_builder.functions import IfNull, Sum
def execute():
active_je_workflow = get_workflow_name("Journal Entry")
if not active_je_workflow:
return
correct_value_for_assets_with_manual_depr_entries()
finance_books = frappe.db.get_all("Finance Book", pluck="name")
if finance_books:
for fb_name in finance_books:
correct_value_for_assets_with_auto_depr(fb_name)
correct_value_for_assets_with_auto_depr()
def correct_value_for_assets_with_manual_depr_entries():
asset = frappe.qb.DocType("Asset")
gle = frappe.qb.DocType("GL Entry")
aca = frappe.qb.DocType("Asset Category Account")
company = frappe.qb.DocType("Company")
asset_details_and_depr_amount_map = (
frappe.qb.from_(gle)
.join(asset)
.on(gle.against_voucher == asset.name)
.join(aca)
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
.join(company)
.on(company.name == asset.company)
.select(
asset.name.as_("asset_name"),
asset.gross_purchase_amount.as_("gross_purchase_amount"),
asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"),
Sum(gle.debit).as_("depr_amount"),
)
.where(
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(asset.docstatus == 1)
.where(asset.calculate_depreciation == 0)
.groupby(asset.name)
)
frappe.qb.update(asset).join(asset_details_and_depr_amount_map).on(
asset_details_and_depr_amount_map.asset_name == asset.name
).set(
asset.value_after_depreciation,
asset_details_and_depr_amount_map.gross_purchase_amount
- asset_details_and_depr_amount_map.opening_accumulated_depreciation
- asset_details_and_depr_amount_map.depr_amount,
).run()
def correct_value_for_assets_with_auto_depr(fb_name=None):
asset = frappe.qb.DocType("Asset")
gle = frappe.qb.DocType("GL Entry")
aca = frappe.qb.DocType("Asset Category Account")
company = frappe.qb.DocType("Company")
afb = frappe.qb.DocType("Asset Finance Book")
asset_details_and_depr_amount_map = (
frappe.qb.from_(gle)
.join(asset)
.on(gle.against_voucher == asset.name)
.join(aca)
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
.join(company)
.on(company.name == asset.company)
.select(
asset.name.as_("asset_name"),
asset.gross_purchase_amount.as_("gross_purchase_amount"),
asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"),
Sum(gle.debit).as_("depr_amount"),
)
.where(
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(asset.docstatus == 1)
.where(asset.calculate_depreciation == 1)
.groupby(asset.name)
)
if fb_name:
asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where(
gle.finance_book == fb_name
)
else:
asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where(
(gle.finance_book.isin([""])) | (gle.finance_book.isnull())
)
query = (
frappe.qb.update(afb)
.join(asset_details_and_depr_amount_map)
.on(asset_details_and_depr_amount_map.asset_name == afb.parent)
.set(
afb.value_after_depreciation,
asset_details_and_depr_amount_map.gross_purchase_amount
- asset_details_and_depr_amount_map.opening_accumulated_depreciation
- asset_details_and_depr_amount_map.depr_amount,
)
)
if fb_name:
query = query.where(afb.finance_book == fb_name)
else:
query = query.where((afb.finance_book.isin([""])) | (afb.finance_book.isnull()))
query.run()

View File

@@ -125,6 +125,7 @@ def execute():
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account

View File

@@ -71,6 +71,12 @@ class Timesheet(Document):
if args.is_billable:
if flt(args.billing_hours) == 0.0:
args.billing_hours = args.hours
elif flt(args.billing_hours) > flt(args.hours):
frappe.msgprint(
_("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx),
indicator="orange",
alert=True,
)
else:
args.billing_hours = 0

View File

@@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", {
frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
frm.trigger('make_issue_from_communication');
})
}, "Create");
}, __("Create"));
}
if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {

View File

@@ -6,8 +6,10 @@ erpnext.financial_statements = {
if (data && column.fieldname=="account") {
value = data.account_name || value;
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
if (data.account) {
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
}
column.is_tree = true;
}

View File

@@ -1378,7 +1378,7 @@ class GSPConnector:
def set_einvoice_data(self, res):
enc_signed_invoice = res.get("SignedInvoice")
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"]
dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})["data"]
self.invoice.irn = res.get("Irn")
self.invoice.ewaybill = res.get("EwbNo")

View File

@@ -532,6 +532,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
primary_action={
"label": "Send Email",
"server_action": "erpnext.selling.doctype.customer.customer.send_emails",
"hide_on_success": True,
"args": {
"customer": customer,
"customer_outstanding": customer_outstanding,

View File

@@ -96,18 +96,26 @@ class SalesOrder(SellingController):
and customer = %s",
(self.po_no, self.name, self.customer),
)
if (
so
and so[0][0]
and not cint(
if so and so[0][0]:
if cint(
frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders")
)
):
frappe.msgprint(
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
so[0][0], self.po_no
):
frappe.msgprint(
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
frappe.bold(so[0][0]), frappe.bold(self.po_no)
)
)
else:
frappe.throw(
_(
"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}"
).format(
frappe.bold(so[0][0]),
frappe.bold(self.po_no),
frappe.bold(_("'Allow Multiple Sales Orders Against a Customer's Purchase Order'")),
get_link_to_form("Selling Settings", "Selling Settings"),
)
)
)
def validate_for_items(self):
for d in self.get("items"):

View File

@@ -1741,7 +1741,7 @@ def make_sales_order(**args):
so.company = args.company or "_Test Company"
so.customer = args.customer or "_Test Customer"
so.currency = args.currency or "INR"
so.po_no = args.po_no or "12345"
so.po_no = args.po_no or ""
if args.selling_price_list:
so.selling_price_list = args.selling_price_list

View File

@@ -3,11 +3,11 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.query_builder import Criterion
from erpnext import get_default_company
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_price_list_rate_for
def execute(filters=None):
@@ -50,6 +50,42 @@ def get_columns(filters=None):
]
def fetch_item_prices(
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
):
price_list_map = frappe._dict()
ip = qb.DocType("Item Price")
and_conditions = []
or_conditions = []
if items:
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
and_conditions.append(ip.selling == True)
or_conditions.append(ip.customer == None)
or_conditions.append(ip.price_list == None)
if customer:
or_conditions.append(ip.customer == customer)
if price_list:
or_conditions.append(ip.price_list == price_list)
if selling_price_list:
or_conditions.append(ip.price_list == selling_price_list)
res = (
qb.from_(ip)
.select(ip.item_code, ip.price_list, ip.price_list_rate)
.where(Criterion.all(and_conditions))
.where(Criterion.any(or_conditions))
.run(as_dict=True)
)
for x in res:
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
return price_list_map
def get_data(filters=None):
data = []
customer_details = get_customer_details(filters)
@@ -59,9 +95,17 @@ def get_data(filters=None):
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
)
item_stock_map = {item.item_code: item.available for item in item_stock_map}
price_list_map = fetch_item_prices(
customer_details.customer,
customer_details.price_list,
customer_details.selling_price_list,
items,
)
for item in items:
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
price_list_rate = price_list_map.get(
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
)
available_stock = item_stock_map.get(item.item_code)
data.append(

View File

@@ -75,7 +75,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
self.assertFalse(exchange_rate == 60)
self.assertEqual(flt(exchange_rate, 3), 66.999)
self.assertEqual(flt(exchange_rate, 3), 66.894)
def test_exchange_rate_strict(self):
# strict currency settings
@@ -87,7 +87,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.235)
self.assertEqual(flt(exchange_rate, 3), 67.79)
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
self.assertEqual(exchange_rate, 62.9)
@@ -95,7 +95,7 @@ class TestCurrencyExchange(unittest.TestCase):
# Exchange rate as on 15th Dec, 2015
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 66.999)
self.assertEqual(flt(exchange_rate, 3), 66.894)
def test_exchange_rate_strict_switched(self):
# Start with allow_stale is True
@@ -108,4 +108,4 @@ class TestCurrencyExchange(unittest.TestCase):
# Will fetch from fixer.io
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.235)
self.assertEqual(flt(exchange_rate, 3), 67.79)

View File

@@ -33,6 +33,7 @@ def after_install():
add_standard_navbar_items()
add_app_name()
add_non_standard_user_types()
update_roles()
frappe.db.commit()
@@ -237,6 +238,12 @@ def create_custom_role(data):
).insert(ignore_permissions=True)
def update_roles():
website_user_roles = ("Customer", "Supplier")
for role in website_user_roles:
frappe.db.set_value("Role", role, "desk_access", 0)
def create_user_type(user_type, data):
if frappe.db.exists("User Type", user_type):
doc = frappe.get_cached_doc("User Type", user_type)

View File

@@ -113,13 +113,30 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if not value:
import requests
api_url = "https://api.exchangerate.host/convert"
response = requests.get(
api_url, params={"date": transaction_date, "from": from_currency, "to": to_currency}
)
if currency_settings.service_provider == "exchangerate.host":
api_url = "https://api.exchangerate.host/convert"
response = requests.get(
api_url,
params={
"access_key": currency_settings.access_key,
"transaction_date": transaction_date,
"amount": 1,
"from": from_currency,
"to": to_currency,
},
)
# exchangerate.host return 200 for all requests. Can't rely on it to raise exception
value = response.json()["result"]
if not response.json()["success"]:
raise frappe.ValidationError
else:
api_url = f"https://api.frankfurter.app/{transaction_date}"
response = requests.get(api_url, params={"from": from_currency, "to": to_currency})
value = response.json()["rates"][to_currency]
# expire in 6 hours
response.raise_for_status()
value = response.json()["result"]
cache.setex(name=key, time=21600, value=flt(value))
return flt(value)
except Exception:

View File

@@ -1,5 +1,4 @@
import frappe
from frappe.utils import cint
def get_leaderboards():
@@ -54,12 +53,13 @@ def get_leaderboards():
@frappe.whitelist()
def get_all_customers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
return frappe.db.get_all(
if from_date and to_date:
filters.append(["posting_date", "between", [from_date, to_date]])
return frappe.get_list(
"Sales Invoice",
fields=["customer as name", "sum(outstanding_amount) as value"],
filters=filters,
@@ -69,26 +69,20 @@ def get_all_customers(date_range, company, field, limit=None):
)
else:
if field == "total_sales_amount":
select_field = "sum(so_item.base_net_amount)"
select_field = "base_net_total"
elif field == "total_qty_sold":
select_field = "sum(so_item.stock_qty)"
select_field = "total_qty"
date_condition = get_date_condition(date_range, "so.transaction_date")
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select so.customer as name, {0} as value
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
ON so.name = so_item.parent
where so.docstatus = 1 {1} and so.company = %s
group by so.customer
order by value DESC
limit %s
""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
return frappe.get_list(
"Sales Order",
fields=["customer as name", f"sum({select_field}) as value"],
filters=filters,
group_by="customer",
order_by="value desc",
limit=limit,
)
@@ -96,55 +90,58 @@ def get_all_customers(date_range, company, field, limit=None):
def get_all_items(date_range, company, field, limit=None):
if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
return frappe.db.get_all(
results = frappe.db.get_all(
"Bin",
fields=["item_code as name", "{0} as value".format(select_field)],
group_by="item_code",
order_by="value desc",
limit=limit,
)
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
return [item for item in results if item["name"] in readable_active_items]
else:
if field == "total_sales_amount":
select_field = "sum(order_item.base_net_amount)"
select_field = "base_net_amount"
select_doctype = "Sales Order"
elif field == "total_purchase_amount":
select_field = "sum(order_item.base_net_amount)"
select_field = "base_net_amount"
select_doctype = "Purchase Order"
elif field == "total_qty_sold":
select_field = "sum(order_item.stock_qty)"
select_field = "stock_qty"
select_doctype = "Sales Order"
elif field == "total_qty_purchased":
select_field = "sum(order_item.stock_qty)"
select_field = "stock_qty"
select_doctype = "Purchase Order"
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select order_item.item_code as name, {0} as value
from `tab{1}` sales_order join `tab{1} Item` as order_item
on sales_order.name = order_item.parent
where sales_order.docstatus = 1
and sales_order.company = %s {2}
group by order_item.item_code
order by value desc
limit %s
""".format(
select_field, select_doctype, date_condition
),
(company, cint(limit)),
as_dict=1,
) # nosec
child_doctype = f"{select_doctype} Item"
return frappe.get_list(
select_doctype,
fields=[
f"`tab{child_doctype}`.item_code as name",
f"sum(`tab{child_doctype}`.{select_field}) as value",
],
filters=filters,
order_by="value desc",
group_by=f"`tab{child_doctype}`.item_code",
limit=limit,
)
@frappe.whitelist()
def get_all_suppliers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
return frappe.db.get_all(
if from_date and to_date:
filters.append(["posting_date", "between", [from_date, to_date]])
return frappe.get_list(
"Purchase Invoice",
fields=["supplier as name", "sum(outstanding_amount) as value"],
filters=filters,
@@ -154,48 +151,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
)
else:
if field == "total_purchase_amount":
select_field = "sum(purchase_order_item.base_net_amount)"
select_field = "base_net_total"
elif field == "total_qty_purchased":
select_field = "sum(purchase_order_item.stock_qty)"
select_field = "total_qty"
date_condition = get_date_condition(date_range, "purchase_order.modified")
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select purchase_order.supplier as name, {0} as value
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
where
purchase_order.docstatus = 1
{1}
and purchase_order.company = %s
group by purchase_order.supplier
order by value DESC
limit %s""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
) # nosec
return frappe.get_list(
"Purchase Order",
fields=["supplier as name", f"sum({select_field}) as value"],
filters=filters,
group_by="supplier",
order_by="value desc",
limit=limit,
)
@frappe.whitelist()
def get_all_sales_partner(date_range, company, field, limit=None):
if field == "total_sales_amount":
select_field = "sum(`base_net_total`)"
select_field = "base_net_total"
elif field == "total_commission":
select_field = "sum(`total_commission`)"
select_field = "total_commission"
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
if date_range:
date_range = frappe.parse_json(date_range)
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.get_list(
"Sales Order",
fields=[
"`sales_partner` as name",
"{} as value".format(select_field),
"sales_partner as name",
f"sum({select_field}) as value",
],
filters=filters,
group_by="sales_partner",
@@ -206,24 +195,25 @@ def get_all_sales_partner(date_range, company, field, limit=None):
@frappe.whitelist()
def get_all_sales_person(date_range, company, field=None, limit=0):
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
filters = [
["docstatus", "=", "1"],
["company", "=", company],
["Sales Team", "sales_person", "is", "set"],
]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
where sales_order.docstatus = 1
and sales_order.company = %s
{date_condition}
group by sales_team.sales_person
order by value DESC
limit %s
""".format(
date_condition=date_condition
),
(company, cint(limit)),
as_dict=1,
return frappe.get_list(
"Sales Order",
fields=[
"`tabSales Team`.sales_person as name",
"sum(`tabSales Team`.allocated_amount) as value",
],
filters=filters,
group_by="`tabSales Team`.sales_person",
order_by="value desc",
limit=limit,
)
@@ -236,3 +226,11 @@ def get_date_condition(date_range, field):
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
)
return date_condition
def parse_date_range(date_range):
if date_range:
date_range = frappe.parse_json(date_range)
return date_range[0], date_range[1]
return None, None

View File

@@ -1268,6 +1268,7 @@
"depends_on": "eval: doc.is_internal_customer",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_standard_filter": 1,
"label": "Set Target Warehouse",
"no_copy": 1,
@@ -1335,7 +1336,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2022-09-16 17:46:17.701904",
"modified": "2023-09-04 14:15:28.363184",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
@@ -1405,4 +1406,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -697,7 +697,7 @@ class TestDeliveryNote(FrappeTestCase):
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order()
so = make_sales_order(po_no="12345")
dn = create_dn_against_so(so.name, delivered_qty=2)
self.assertEqual(dn.status, "To Bill")
@@ -724,7 +724,7 @@ class TestDeliveryNote(FrappeTestCase):
make_sales_invoice,
)
so = make_sales_order()
so = make_sales_order(po_no="12345")
si = make_sales_invoice(so.name)
si.get("items")[0].qty = 5
@@ -768,7 +768,7 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
so = make_sales_order()
so = make_sales_order(po_no="12345")
dn1 = make_delivery_note(so.name)
dn1.get("items")[0].qty = 2
@@ -814,7 +814,7 @@ class TestDeliveryNote(FrappeTestCase):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order()
so = make_sales_order(po_no="12345")
si = make_sales_invoice(so.name)
si.submit()
@@ -1180,6 +1180,25 @@ class TestDeliveryNote(FrappeTestCase):
self.assertTrue(return_dn.docstatus == 1)
def tearDown(self):
frappe.db.rollback()
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
def test_non_internal_transfer_delivery_note(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
dn = create_delivery_note(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company)
dn.items[0].db_set("target_warehouse", warehouse)
dn.reload()
self.assertEqual(dn.items[0].target_warehouse, warehouse)
dn.save()
dn.reload()
self.assertFalse(dn.items[0].target_warehouse)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -742,6 +742,8 @@ def create_item(
opening_stock=0,
is_fixed_asset=0,
asset_category=None,
buying_cost_center=None,
selling_cost_center=None,
company="_Test Company",
):
if not frappe.db.exists("Item", item_code):
@@ -759,7 +761,15 @@ def create_item(
item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or ""
item.append("item_defaults", {"default_warehouse": warehouse, "company": company})
item.append(
"item_defaults",
{
"default_warehouse": warehouse,
"company": company,
"selling_cost_center": selling_cost_center,
"buying_cost_center": buying_cost_center,
},
)
item.save()
else:
item = frappe.get_doc("Item", item_code)

View File

@@ -1069,88 +1069,6 @@ class TestPurchaseReceipt(FrappeTestCase):
pr1.reload()
pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self):
pr1 = make_purchase_receipt(
warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory"
)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
)
pr.supplier_warehouse = ""
pr.items[0].from_warehouse = "Work In Progress - TCP1"
pr.submit()
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5}
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
pr.cancel()
pr1.cancel()
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
create_warehouse(
"_Test Warehouse for Valuation",
company="_Test Company with perpetual inventory",
properties={"account": "_Test Account Stock In Hand - TCP1"},
)
pr1 = make_purchase_receipt(
warehouse="_Test Warehouse for Valuation - TCP1",
company="_Test Company with perpetual inventory",
)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
)
pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
pr.supplier_warehouse = ""
pr.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Shipping Charges - TCP1",
"category": "Valuation and Total",
"cost_center": "Main - TCP1",
"description": "Test",
"rate": 9,
},
)
pr.submit()
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
expected_gle = [
["Stock In Hand - TCP1", 272.5, 0.0],
["_Test Account Stock In Hand - TCP1", 0.0, 250.0],
["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
]
expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5}
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
for i, gle in enumerate(gl_entries):
self.assertEqual(gle.account, expected_gle[i][0])
self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2])
pr.cancel()
pr1.cancel()
def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt,
@@ -1996,6 +1914,75 @@ class TestPurchaseReceipt(FrappeTestCase):
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, valuation_rate)
def test_valuation_rate_in_return_purchase_receipt_for_moving_average(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.stock_ledger import get_previous_sle
# Step - 1: Create an Item (Valuation Method = Moving Average)
item_code = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name
# Step - 2: Create a Purchase Receipt (Qty = 10, Rate = 100)
pr = make_purchase_receipt(qty=10, rate=100, item_code=item_code)
# Step - 3: Create a Material Receipt Stock Entry (Qty = 100, Basic Rate = 10)
warehouse = "_Test Warehouse - _TC"
make_stock_entry(
purpose="Material Receipt",
item_code=item_code,
to_warehouse=warehouse,
qty=100,
rate=10,
)
# Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched])
make_stock_entry(
purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100
)
# Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched])
return_pr = make_purchase_receipt(
is_return=1,
return_against=pr.name,
item_code=item_code,
qty=-8,
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": return_pr.name, "voucher_detail_no": return_pr.items[0].name},
["posting_date", "posting_time", "outgoing_rate", "valuation_rate"],
as_dict=1,
)
previous_sle_valuation_rate = get_previous_sle(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
}
).get("valuation_rate")
# Test - 1: Valuation Rate should be equal to Outgoing Rate
self.assertEqual(flt(sle.outgoing_rate, 2), flt(sle.valuation_rate, 2))
# Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate
self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2))
def non_internal_transfer_purchase_receipt(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
pr_doc = make_purchase_receipt(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company)
pr_doc.items[0].db_set("target_warehouse", "warehouse")
pr_doc.reload()
self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name)
pr_doc.save()
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -5,7 +5,7 @@
from unittest.mock import MagicMock, call
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from frappe.utils.data import add_to_date, today
@@ -173,6 +173,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
riv.set_status("Skipped")
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_prevention_of_cancelled_transaction_riv(self):
frappe.flags.dont_execute_stock_reposts = True
@@ -295,6 +296,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
accounts_settings.acc_frozen_upto = ""
accounts_settings.save()
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_create_repost_entry_for_cancelled_document(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",

View File

@@ -1414,6 +1414,7 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_reposting_for_depedent_warehouse(self):
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

View File

@@ -48,7 +48,7 @@
"label": "Limit timeslot for Stock Reposting"
},
{
"default": "0",
"default": "1",
"fieldname": "item_based_reposting",
"fieldtype": "Check",
"label": "Use Item based reposting"
@@ -57,7 +57,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-11-02 01:22:45.155841",
"modified": "2023-11-01 16:14:29.080697",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
@@ -77,4 +77,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -751,6 +751,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
data = frappe.get_attr(path)(args.get("item_code"), company)
if data and (data.selling_cost_center or data.buying_cost_center):
if args.get("customer") and data.selling_cost_center:
return data.selling_cost_center
elif args.get("supplier") and data.buying_cost_center:
return data.buying_cost_center
return data.selling_cost_center or data.buying_cost_center
if not cost_center and args.get("cost_center"):

View File

@@ -33,5 +33,43 @@ frappe.query_reports["Stock and Account Value Comparison"] = {
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
},
]
],
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__("Create Reposting Entries"), function() {
let message = `
<div>
<p>
Reposting Entries will change the value of
accounts Stock In Hand, and Stock Expenses
in the Trial Balance report and will also change
the Balance Value in the Stock Balance report.
</p>
<p>Are you sure you want to create Reposting Entries?</p>
</div>`;
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select rows to create Reposting Entries"));
}
frappe.confirm(__(message), () => {
frappe.call({
method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries",
args: {
rows: selected_rows,
company: frappe.query_report.get_filter_values().company
}
});
});
});
}
};

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
import erpnext
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
@@ -134,3 +135,35 @@ def get_columns(filters):
"width": "120",
},
]
@frappe.whitelist()
def create_reposting_entries(rows, company):
if isinstance(rows, str):
rows = parse_json(rows)
entries = []
for row in rows:
row = frappe._dict(row)
try:
doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Transaction",
"status": "Queued",
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"posting_date": row.posting_date,
"company": company,
"allow_nagative_stock": 1,
}
).submit()
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
except frappe.DuplicateEntryError:
pass
if entries:
entries = ", ".join(entries)
frappe.msgprint(_("Reposting entries created: {0}").format(entries))

View File

@@ -3,23 +3,23 @@
/* eslint-disable */
const DIFFERNCE_FIELD_NAMES = [
"difference_in_qty",
"fifo_qty_diff",
"fifo_value_diff",
"fifo_valuation_diff",
"valuation_diff",
"fifo_difference_diff",
"diff_value_diff"
'difference_in_qty',
'fifo_qty_diff',
'fifo_value_diff',
'fifo_valuation_diff',
'valuation_diff',
'fifo_difference_diff',
'diff_value_diff'
];
frappe.query_reports["Stock Ledger Invariant Check"] = {
"filters": [
frappe.query_reports['Stock Ledger Invariant Check'] = {
'filters': [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"mandatory": 1,
"options": "Item",
'fieldname': 'item_code',
'fieldtype': 'Link',
'label': 'Item',
'mandatory': 1,
'options': 'Item',
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
@@ -27,18 +27,61 @@ frappe.query_reports["Stock Ledger Invariant Check"] = {
}
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory": 1,
"options": "Warehouse",
'fieldname': 'warehouse',
'fieldtype': 'Link',
'label': 'Warehouse',
'mandatory': 1,
'options': 'Warehouse',
}
],
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
value = "<span style='color:red'>" + value + "</span>";
value = '<span style="color:red">' + value + '</span>';
}
return value;
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__('Create Reposting Entry'), () => {
let message = `
<div>
<p>
Reposting Entry will change the value of
accounts Stock In Hand, and Stock Expenses
in the Trial Balance report and will also change
the Balance Value in the Stock Balance report.
</p>
<p>Are you sure you want to create a Reposting Entry?</p>
</div>`;
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__('Please select a row to create a Reposting Entry'));
}
else if (selected_rows.length > 1) {
frappe.throw(__('Please select only one row to create a Reposting Entry'));
}
else {
frappe.confirm(__(message), () => {
frappe.call({
method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
args: {
rows: selected_rows,
item_code: frappe.query_report.get_filter_values().item_code,
warehouse: frappe.query_report.get_filter_values().warehouse,
}
});
});
}
});
},
};

View File

@@ -4,6 +4,8 @@
import json
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
SLE_FIELDS = (
"name",
@@ -247,3 +249,35 @@ def get_columns():
"label": "H - J",
},
]
@frappe.whitelist()
def create_reposting_entries(rows, item_code=None, warehouse=None):
if isinstance(rows, str):
rows = parse_json(rows)
entries = []
for row in rows:
row = frappe._dict(row)
try:
doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Item and Warehouse",
"status": "Queued",
"item_code": item_code or row.item_code,
"warehouse": warehouse or row.warehouse,
"posting_date": row.posting_date,
"posting_time": row.posting_time,
"allow_nagative_stock": 1,
}
).submit()
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
except frappe.DuplicateEntryError:
continue
if entries:
entries = ", ".join(entries)
frappe.msgprint(_("Reposting entries created: {0}").format(entries))

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
const DIFFERENCE_FIELD_NAMES = [
"difference_in_qty",
"fifo_qty_diff",
"fifo_value_diff",
"fifo_valuation_diff",
"valuation_diff",
"fifo_difference_diff",
"diff_value_diff"
];
frappe.query_reports["Stock Ledger Variance"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": __("Item"),
"options": "Item",
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
}
}
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": __("Warehouse"),
"options": "Warehouse",
get_query: function() {
return {
filters: {is_group: 0, disabled: 0}
}
}
},
{
"fieldname": "difference_in",
"fieldtype": "Select",
"label": __("Difference In"),
"options": [
"",
"Qty",
"Value",
"Valuation",
],
},
{
"fieldname": "include_disabled",
"fieldtype": "Check",
"label": __("Include Disabled"),
}
],
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
value = "<span style='color:red'>" + value + "</span>";
}
return value;
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__('Create Reposting Entries'), () => {
let message = `
<div>
<p>
Reposting Entries will change the value of
accounts Stock In Hand, and Stock Expenses
in the Trial Balance report and will also change
the Balance Value in the Stock Balance report.
</p>
<p>Are you sure you want to create Reposting Entries?</p>
</div>`;
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select rows to create Reposting Entries"));
}
frappe.confirm(__(message), () => {
frappe.call({
method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
args: {
rows: selected_rows,
}
});
});
});
},
};

View File

@@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2023-09-20 10:44:19.414449",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-09-20 10:44:19.414449",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Variance",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Stock Ledger Variance",
"report_type": "Script Report",
"roles": []
}

View File

@@ -0,0 +1,290 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import cint, flt
from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import (
get_data as stock_ledger_invariant_check,
)
def execute(filters=None):
columns, data = [], []
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "name",
"fieldtype": "Link",
"label": _("Stock Ledger Entry"),
"options": "Stock Ledger Entry",
},
{
"fieldname": "posting_date",
"fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
"fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
"fieldtype": "Data",
"label": _("Creation"),
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": _("Item"),
"options": "Item",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": _("Warehouse"),
"options": "Warehouse",
},
{
"fieldname": "valuation_method",
"fieldtype": "Data",
"label": _("Valuation Method"),
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": _("Voucher Type"),
"options": "DocType",
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": _("Voucher No"),
"options": "voucher_type",
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
"label": _("Batchwise Valuation"),
},
{
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": _("Qty Change"),
},
{
"fieldname": "incoming_rate",
"fieldtype": "Float",
"label": _("Incoming Rate"),
},
{
"fieldname": "consumption_rate",
"fieldtype": "Float",
"label": _("Consumption Rate"),
},
{
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"label": _("(A) Qty After Transaction"),
},
{
"fieldname": "expected_qty_after_transaction",
"fieldtype": "Float",
"label": _("(B) Expected Qty After Transaction"),
},
{
"fieldname": "difference_in_qty",
"fieldtype": "Float",
"label": _("A - B"),
},
{
"fieldname": "stock_queue",
"fieldtype": "Data",
"label": _("FIFO/LIFO Queue"),
},
{
"fieldname": "fifo_queue_qty",
"fieldtype": "Float",
"label": _("(C) Total Qty in Queue"),
},
{
"fieldname": "fifo_qty_diff",
"fieldtype": "Float",
"label": _("A - C"),
},
{
"fieldname": "stock_value",
"fieldtype": "Float",
"label": _("(D) Balance Stock Value"),
},
{
"fieldname": "fifo_stock_value",
"fieldtype": "Float",
"label": _("(E) Balance Stock Value in Queue"),
},
{
"fieldname": "fifo_value_diff",
"fieldtype": "Float",
"label": _("D - E"),
},
{
"fieldname": "stock_value_difference",
"fieldtype": "Float",
"label": _("(F) Change in Stock Value"),
},
{
"fieldname": "stock_value_from_diff",
"fieldtype": "Float",
"label": _("(G) Sum of Change in Stock Value"),
},
{
"fieldname": "diff_value_diff",
"fieldtype": "Float",
"label": _("G - D"),
},
{
"fieldname": "fifo_stock_diff",
"fieldtype": "Float",
"label": _("(H) Change in Stock Value (FIFO Queue)"),
},
{
"fieldname": "fifo_difference_diff",
"fieldtype": "Float",
"label": _("H - F"),
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
"label": _("(I) Valuation Rate"),
},
{
"fieldname": "fifo_valuation_rate",
"fieldtype": "Float",
"label": _("(J) Valuation Rate as per FIFO"),
},
{
"fieldname": "fifo_valuation_diff",
"fieldtype": "Float",
"label": _("I - J"),
},
{
"fieldname": "balance_value_by_qty",
"fieldtype": "Float",
"label": _("(K) Valuation = Value (D) ÷ Qty (A)"),
},
{
"fieldname": "valuation_diff",
"fieldtype": "Float",
"label": _("I - K"),
},
]
def get_data(filters=None):
filters = frappe._dict(filters or {})
item_warehouse_map = get_item_warehouse_combinations(filters)
valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
data = []
if item_warehouse_map:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item_warehouse in item_warehouse_map:
report_data = stock_ledger_invariant_check(item_warehouse)
if not report_data:
continue
for row in report_data:
if has_difference(
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
):
row.update(
{
"item_code": item_warehouse.item_code,
"warehouse": item_warehouse.warehouse,
"valuation_method": item_warehouse.valuation_method or valuation_method,
}
)
data.append(row)
break
return data
def get_item_warehouse_combinations(filters: dict = None) -> dict:
filters = frappe._dict(filters or {})
bin = frappe.qb.DocType("Bin")
item = frappe.qb.DocType("Item")
warehouse = frappe.qb.DocType("Warehouse")
query = (
frappe.qb.from_(bin)
.inner_join(item)
.on(bin.item_code == item.name)
.inner_join(warehouse)
.on(bin.warehouse == warehouse.name)
.select(
bin.item_code,
bin.warehouse,
item.valuation_method,
)
.where(
(item.is_stock_item == 1)
& (item.has_serial_no == 0)
& (warehouse.is_group == 0)
& (warehouse.company == filters.company)
)
)
if filters.item_code:
query = query.where(item.name == filters.item_code)
if filters.warehouse:
query = query.where(warehouse.name == filters.warehouse)
if not filters.include_disabled:
query = query.where((item.disabled == 0) & (warehouse.disabled == 0))
return query.run(as_dict=1)
def has_difference(row, precision, difference_in, valuation_method):
if valuation_method == "Moving Average":
qty_diff = flt(row.difference_in_qty, precision)
value_diff = flt(row.diff_value_diff, precision)
valuation_diff = flt(row.valuation_diff, precision)
else:
qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
value_diff = (
flt(row.diff_value_diff, precision)
or flt(row.fifo_value_diff, precision)
or flt(row.fifo_difference_diff, precision)
)
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
if difference_in == "Qty" and qty_diff:
return True
elif difference_in == "Value" and value_diff:
return True
elif difference_in == "Valuation" and valuation_diff:
return True
elif difference_in not in ["Qty", "Value", "Valuation"] and (
qty_diff or value_diff or valuation_diff
):
return True

View File

@@ -503,7 +503,7 @@ class update_entries_after(object):
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
val = frappe._dict({"sle": dependant_sle})
if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val
@@ -517,6 +517,8 @@ class update_entries_after(object):
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
@@ -656,14 +658,16 @@ class update_entries_after(object):
get_rate_for_return, # don't move this import to top
)
rate = get_rate_for_return(
sle.voucher_type,
sle.voucher_no,
sle.item_code,
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
if self.valuation_method == "Moving Average":
rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate)
else:
rate = get_rate_for_return(
sle.voucher_type,
sle.voucher_no,
sle.item_code,
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no

View File

@@ -7,7 +7,7 @@
{% if d.thumbnail or d.image %}
{{ product_image(d.thumbnail or d.image, no_border=True) }}
{% else %}
<div class="no-image-cart-item" style="min-height: 100px;">
<div class="no-image-cart-item" style="min-height: 50px;">
{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
</div>
{% endif %}

View File

@@ -81,7 +81,7 @@ rfq = Class.extend({
doc: doc
},
btn: this,
callback: function(r){
callback: function(r) {
frappe.unfreeze();
if(r.message){
$('.btn-sm').hide()

View File

@@ -1,19 +1,25 @@
{% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %}
{% macro item_name_and_description(d, doc) %}
<div class="row">
<div class="col-3">
{{ product_image(d.image) }}
</div>
<div class="col-9">
{{ d.item_code }}
<p class="text-muted small">{{ d.description }}</p>
<div class="row">
<div class="col-3">
{% if d.image %}
{{ product_image(d.image) }}
{% else %}
<div class="website-image h-100 w-100" style="background-color:var(--gray-100);text-align: center;line-height: 3.6;">
{{ frappe.utils.get_abbr(d.item_name)}}
</div>
{% endif %}
</div>
<div class="col-9">
{{ d.item_code }}
<p class="text-muted small">{{ d.description }}</p>
{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}
<p class="text-muted small supplier-part-no">
{% if supplier_part_no %}
{{_("Supplier Part No") + ": "+ supplier_part_no}}
{% endif %}
</p>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -153,7 +153,6 @@
</div>
{% endif %}
{% if attachments %}
<div class="order-item-table">
<div class="row order-items order-item-header text-muted">
@@ -181,6 +180,7 @@
{% endif %}
{% endblock %}
{% block script %}
<script> {% include "templates/pages/order.js" %} </script>
<script>

View File

@@ -1,7 +1,7 @@
{% extends "templates/web.html" %}
{% block header %}
<h1>{{ doc.name }}</h1>
<h1 style="margin-top: 10px;">{{ doc.name }}</h1>
{% endblock %}
{% block script %}
@@ -16,7 +16,7 @@
{% if doc.items %}
<button class="btn btn-primary btn-sm"
type="button">
{{ _("Submit") }}</button>
{{ _("Make Quotation") }}</button>
{% endif %}
{% endblock %}

View File

@@ -369,7 +369,7 @@ Base,Basis,
Base URL,Basis-URL,
Based On,Basiert auf,
Based On Payment Terms,Basierend auf Zahlungsbedingungen,
Basic,Grundeinkommen,
Basic,Basic,
Batch,Charge,
Batch Entries,Batch-Einträge,
Batch ID is mandatory,Batch-ID ist obligatorisch,
Can't render this file because it is too large.

View File

@@ -4,7 +4,7 @@ googlemaps # used in ERPNext, but dependency is defined in Frappe
pandas>=1.1.5,<2.0.0
plaid-python~=7.2.1
pycountry~=20.7.3
PyGithub~=1.54.1
PyGithub~=2.1.1
python-stdnum~=1.16
python-youtube~=0.8.0
taxjar~=1.9.2