Compare commits

..

110 Commits

Author SHA1 Message Date
Frappe PR Bot
69087b8790 chore(release): Bumped to Version 13.36.1
## [13.36.1](https://github.com/frappe/erpnext/compare/v13.36.0...v13.36.1) (2022-07-21)

### Bug Fixes

* ensure defaults removed in bad frappe patch get set again (backport [#31659](https://github.com/frappe/erpnext/issues/31659)) ([2ae3bd9](2ae3bd9fed))
2022-07-21 08:26:20 +00:00
mergify[bot]
2ae3bd9fed fix: ensure defaults removed in bad frappe patch get set again (backport #31659)
fix: ensure defaults removed in bad frappe patch get set again (#31659)

(cherry picked from commit bf2833b8ee)
(cherry picked from commit 9071018e4e)

Co-authored-by: Sagar Vora <sagar@resilient.tech>
2022-07-21 13:54:40 +05:30
Frappe PR Bot
32ba8c7d6d chore(release): Bumped to Version 13.36.0
# [13.36.0](https://github.com/frappe/erpnext/compare/v13.35.3...v13.36.0) (2022-07-20)

### Bug Fixes

* (india) (e-invoice) allow generation for UIN Holders ([82539fc](82539fc18a))
* (india) (e-invoice) CN / DN with 0 qty ([84f260e](84f260e1d8))
* (india) (e-invoice) discount calculation ([1f4d434](1f4d434675))
* (india) (e-invoice) discount in CN/DN ([6e6b55e](6e6b55ece0))
* (india) (e-invoice) qty should not be changed to 1 ([b0efb98](b0efb98237))
* (india)(einvoice) discount for CN DN ([844758a](844758a27c))
* added Section translation in German for Salary Slip (backport [#31608](https://github.com/frappe/erpnext/issues/31608)) ([#31616](https://github.com/frappe/erpnext/issues/31616)) ([12d3d6a](12d3d6ab49))
* Allow multi currency invoice against single party account ([9ac9c46](9ac9c46dab))
* **India:** Inward supplies from Composition Supplier in 3B report ([b147ce4](b147ce4206))
* LCV updates wrong future qty/Bin qty ([f0ecdbe](f0ecdbef5a))
* linter ([9a3c846](9a3c84663f))
* move auto attendance job to long queue ([8eb9aaa](8eb9aaafe9))
* on cancel for loan refund ([8237d94](8237d94b11))
* Opening Invoice Creation Tool msgprint ([cb80bcd](cb80bcd0ee))
* Patch to make accounting dimension in orders ([55399f2](55399f2e58))
* Remove unnecessary list comprehensions ([711501b](711501be5e))
* Removed 'Allow Monthly Depreciation' checkbox and fixed wdv depreciation rate ([616e3c6](616e3c66b9))
* Renamed status field to gle_processing_status ([1180135](1180135067))
* Set Amount in Supplied Item table ([72e8c02](72e8c02ae0))
* set default_bom for item ([def622c](def622c13e))
* set status on submit/cancel ([04b077a](04b077a89f))
* slow stock reposting ([a5dae92](a5dae9264d))
* Supplier details in TDS monthly report ([#31599](https://github.com/frappe/erpnext/issues/31599)) ([431d79f](431d79f516))
* Tax amount not considered in Expense Claim Journal Entry ([90c751f](90c751f648))
* test ([9631ffd](9631ffd215))

### Features

* add adjustment amount to loan ([d5ce780](d5ce780e87))

### Performance Improvements

* index shift type and employee in checkins and assignment to avoid full table scans ([6938025](6938025952))
* Optimization of gl entry processing logic in period closing voucher ([903e42f](903e42fbdd))
* Replace `db.get_all` with `db.exists` in `is_holiday` ([85c554e](85c554eab5))
* Replace `get_doc` with `db.get_value` in `get_shift_details` ([abb7ac5](abb7ac5a0b))
* Use `get_cached_value` and `get_cached_doc` ([8c9035d](8c9035d914))
2022-07-20 15:21:11 +00:00
Deepesh Garg
4d67695294 Merge pull request #31647 from frappe/version-13-hotfix
chore: weekly version-13 release
2022-07-20 20:49:09 +05:30
Deepesh Garg
50fbfd9839 Merge pull request #31649 from frappe/mergify/bp/version-13-hotfix/pr-31648
fix: Patch to make accounting dimension in orders (backport #31648)
2022-07-20 20:01:27 +05:30
Deepesh Garg
e36c74ac44 chore: resolve conflicts 2022-07-20 19:39:18 +05:30
Deepesh Garg
45e25b2d56 chore: do not re run patch
(cherry picked from commit cad2035e07)

# Conflicts:
#	erpnext/patches.txt
2022-07-20 13:32:41 +00:00
Deepesh Garg
55399f2e58 fix: Patch to make accounting dimension in orders
(cherry picked from commit d46e406db7)

# Conflicts:
#	erpnext/patches.txt
2022-07-20 13:32:41 +00:00
Deepesh Garg
a5cd81c73c Merge branch 'version-13' into version-13-hotfix 2022-07-20 17:52:28 +05:30
Nabin Hait
52c8e0a6ea Merge pull request #31644 from nabinhait/asset-monthly-depreciation-v13-hotfix
fix: Removed 'Allow Monthly Depreciation' checkbox
2022-07-20 17:33:15 +05:30
Nabin Hait
616e3c66b9 fix: Removed 'Allow Monthly Depreciation' checkbox and fixed wdv depreciation rate 2022-07-20 17:32:28 +05:30
Nabin Hait
a6e97ea9ab Merge pull request #31626 from nabinhait/pcv-perf-enhancements
perf: Optimization of gl entry processing logic in period closing voucher
2022-07-20 15:39:39 +05:30
Deepesh Garg
2c5e117f01 Merge pull request #31629 from maharshivpatel/fix-einvoice-discounts
fix: (india) (e-invoice) convert discount to absolute value
2022-07-20 13:18:14 +05:30
Marica
a65dd58444 Merge pull request #31416 from frappe/mergify/bp/version-13-hotfix/pr-31412
fix: set default_bom for item (backport #31412)
2022-07-20 11:31:55 +05:30
Nabin Hait
04b077a89f fix: set status on submit/cancel 2022-07-20 11:10:36 +05:30
Nabin Hait
1180135067 fix: Renamed status field to gle_processing_status 2022-07-20 10:50:44 +05:30
Rucha Mahabal
eae4b06a80 Merge pull request #31630 from ruchamahabal/auto-attendance-perf 2022-07-20 10:25:02 +05:30
Maharshi Patel
b0efb98237 fix: (india) (e-invoice) qty should not be changed to 1
Qty of 0 is allowed so can't change item.qty to 1  instead created item_qty and used it.
2022-07-19 22:29:38 +05:30
rohitwaghchaure
e7e1847ec9 Merge pull request #31634 from frappe/mergify/bp/version-13-hotfix/pr-31631
fix: slow stock reposting (backport #31631)
2022-07-19 19:26:35 +05:30
Rohit Waghchaure
a5dae9264d fix: slow stock reposting
(cherry picked from commit 1d80d37ccf)
2022-07-19 13:18:13 +00:00
Marica
284095106c Merge pull request #31588 from SolufyPrivateLimited/Solufyin-oict
fix: Opening Invoice Creation Tool msgprint
2022-07-19 17:47:44 +05:30
Rucha Mahabal
8eb9aaafe9 fix: move auto attendance job to long queue 2022-07-19 16:07:18 +05:30
Rucha Mahabal
6938025952 perf: index shift type and employee in checkins and assignment to avoid full table scans 2022-07-19 16:02:31 +05:30
Maharshi Patel
6e6b55ece0 fix: (india) (e-invoice) discount in CN/DN
discount fixes
2022-07-19 15:31:34 +05:30
Rucha Mahabal
8c9035d914 perf: Use get_cached_value and get_cached_doc 2022-07-19 14:47:14 +05:30
Nabin Hait
cfb11f4b84 test: Added test for PCV cancellation 2022-07-19 14:31:02 +05:30
Marica
feffc3af27 Merge pull request #31627 from frappe/mergify/bp/version-13-hotfix/pr-31515
fix: LCV updates wrong future qty/Bin qty (backport #31515)
2022-07-19 14:21:33 +05:30
marination
ca9c6e1651 test: LCV impact on future stock balancees
(cherry picked from commit de9ea70ce3)
2022-07-19 08:10:22 +00:00
marination
f0ecdbef5a fix: LCV updates wrong future qty/Bin qty
- As -ve LCV SLE case is returned from `repost_current_voucher`, future qty is not updated
- This just doubly shifts all future qty which is then fixed by a repost
- Until the repost balance values are wrong
- Bin continues to show wrong projected qty even after repost, this is fixed by next SLE that recalculates Bin

(cherry picked from commit 7a5fd71a6c)
2022-07-19 08:10:21 +00:00
Marica
31930a16fa Merge pull request #31624 from marination/sub-contract-amount
fix: Set `amount` in Raw Supplied Items table
2022-07-19 12:55:20 +05:30
Deepesh Garg
4f55fef782 Merge pull request #31623 from frappe/mergify/bp/version-13-hotfix/pr-31599
fix: Supplier details in TDS monthly report (backport #31599)
2022-07-19 12:20:29 +05:30
marination
72e8c02ae0 fix: Set Amount in Supplied Item table 2022-07-19 11:47:25 +05:30
Deepesh Garg
f02596242c Merge pull request #31609 from deepeshgarg007/gstr_3b_gst_category
fix(India): Inward supplies from Composition Supplier in GST 3B report
2022-07-19 10:23:44 +05:30
Deepesh Garg
431d79f516 fix: Supplier details in TDS monthly report (#31599)
(cherry picked from commit a6ff4db2ec)
2022-07-19 04:45:28 +00:00
Rucha Mahabal
711501be5e fix: Remove unnecessary list comprehensions 2022-07-18 21:32:04 +05:30
Rucha Mahabal
abb7ac5a0b perf: Replace get_doc with db.get_value in get_shift_details 2022-07-18 21:13:02 +05:30
Rucha Mahabal
85c554eab5 perf: Replace db.get_all with db.exists in is_holiday
- widely used function call, fetching all rows for holidays
2022-07-18 21:08:15 +05:30
Nabin Hait
903e42fbdd perf: Optimization of gl entry processing logic in period closing voucher 2022-07-18 17:59:42 +05:30
mergify[bot]
12d3d6ab49 fix: added Section translation in German for Salary Slip (backport #31608) (#31616)
Co-authored-by: Wolfram Schmidt <wolfram.schmidt@phamos.eu>
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-07-18 13:41:42 +05:30
Rucha Mahabal
26497f9c0e Merge pull request #31614 from ruchamahabal/fix-claim-payment
fix: Tax amount not considered in Expense Claim Journal Entry
2022-07-18 13:24:12 +05:30
Rucha Mahabal
6a7549d4b2 test: Journal Entry against Expense Claim 2022-07-18 12:57:50 +05:30
Rucha Mahabal
90c751f648 fix: Tax amount not considered in Expense Claim Journal Entry
- add a helper function `get_outstanding_amount_for_claim` for uniformity in usages
2022-07-18 12:39:09 +05:30
Frappe PR Bot
1090862296 chore(release): Bumped to Version 13.35.3
## [13.35.3](https://github.com/frappe/erpnext/compare/v13.35.2...v13.35.3) (2022-07-18)

### Bug Fixes

* (india) (e-invoice) CN / DN with 0 qty and discount issues ([425abcd](425abcdea7))
2022-07-18 05:21:17 +00:00
mergify[bot]
425abcdea7 fix: (india) (e-invoice) CN / DN with 0 qty and discount issues
* fix: (india) (e-invoice) CN / DN with 0 qty
2022-07-18 10:49:41 +05:30
Deepesh Garg
144057f7e3 Merge pull request #31583 from maharshivpatel/fix-e_invoice-cn-dn-qty-0-error
fix: (india) (e-invoice) CN / DN with 0 qty and discount issues
2022-07-18 09:09:19 +05:30
Maharshi Patel
9631ffd215 fix: test
change setting to run test with correct use case.
2022-07-17 15:40:55 +05:30
Maharshi Patel
9a3c84663f fix: linter 2022-07-17 15:22:37 +05:30
Maharshi Patel
2066e5a53a discount settings for e-invoice added and test cases 2022-07-17 15:08:40 +05:30
Deepesh Garg
b147ce4206 fix(India): Inward supplies from Composition Supplier in 3B report 2022-07-17 00:43:25 +05:30
Deepesh Garg
0d6beed546 test: Update test case 2022-07-16 14:06:01 +05:30
Maharshi Patel
844758a27c fix: (india)(einvoice) discount for CN DN 2022-07-15 16:54:10 +05:30
Maharshi Patel
1f4d434675 fix: (india) (e-invoice) discount calculation
I have added fixes for discount in e-inovice.
2022-07-15 16:03:42 +05:30
Deepesh Garg
8c7b836de8 Merge pull request #31595 from maharshivpatel/fix-uin-e_invoice
fix: (india) (e-invoice) allow generation for UIN Holders
2022-07-14 21:25:45 +05:30
Deepesh Garg
9fd1fad259 Merge pull request #31572 from frappe/mergify/bp/version-13-hotfix/pr-31566
fix: Allow multi currency invoice against single party account (backport #31566)
2022-07-14 21:25:17 +05:30
Deepesh Garg
dfa0638baf Merge pull request #31577 from frappe/mergify/bp/version-13-hotfix/pr-31543
feat: Loan balance adjustment doctypes (backport #31543)
2022-07-14 21:24:49 +05:30
Deepesh Garg
0a5bc86d1b chore: resolve conflicts 2022-07-14 17:20:32 +05:30
Deepesh Garg
bdd15895ff chore: resolve conflicts 2022-07-14 17:06:01 +05:30
Maharshi Patel
82539fc18a fix: (india) (e-invoice) allow generation for UIN Holders
e-invoice is required for UIN Holders and they should be treated as Registered Regular. there was incorrect if hasattr check that prevented UIN number validation.
2022-07-14 15:12:51 +05:30
Solufyin
cb80bcd0ee fix: Opening Invoice Creation Tool msgprint 2022-07-13 11:55:47 +05:30
Maharshi Patel
84f260e1d8 fix: (india) (e-invoice) CN / DN with 0 qty
Qty 0 is allowed when creating Credit or Debit Note this caused ZeroDivisionError during unit_rate calculation.
fixed the issue by adding required conditionals.
2022-07-13 01:39:36 +05:30
Sagar Sharma
023c5db3bc Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-31412 2022-07-12 23:33:48 +05:30
Deepesh Garg
0b3958d692 chore: Linting Issues
(cherry picked from commit 0bac030ca7)
2022-07-12 12:56:46 +00:00
Deepesh Garg
c2a7dc7754 chore: Linting Issues
(cherry picked from commit d933ff5cf6)
2022-07-12 12:56:46 +00:00
Abhinav Raut
8237d94b11 fix: on cancel for loan refund
(cherry picked from commit 13b7ed1e2c)
2022-07-12 12:56:45 +00:00
Labeeb Mattra
0713a240a7 Consider refund_amount in pending principal amount
(cherry picked from commit 35f2717ad2)
2022-07-12 12:56:45 +00:00
Labeeb Mattra
61b2a1c3cb fix excess amount calculation in loan refund
(cherry picked from commit 9df1413adb)
2022-07-12 12:56:44 +00:00
Labeeb Mattra
94be7a95f7 Update adjustment_type field options
(cherry picked from commit 245b0c7818)
2022-07-12 12:56:44 +00:00
Labeeb Mattra
7ba0326bdd fix adjustment amount field name
(cherry picked from commit 6cc09ef3a2)
2022-07-12 12:56:43 +00:00
Labeeb Mattra
86af1ef6ed fix indent and imports
(cherry picked from commit 0ed6382ab6)
2022-07-12 12:56:43 +00:00
Labeeb Mattra
b995338d0c fix lint
(cherry picked from commit 8434ec09c3)
2022-07-12 12:56:43 +00:00
Labeeb Mattra
0856e14c13 Use adjustment amounts in pending principal amnt
(cherry picked from commit 1b5b2138ee)
2022-07-12 12:56:42 +00:00
Labeeb Mattra
ff5ee2bbac Add ref no to balance adjustment remarks
(cherry picked from commit a1a51ce1a6)
2022-07-12 12:56:42 +00:00
Labeeb Mattra
a36e8a83d8 Add reference number to repayment remarks
(cherry picked from commit 74dbf8c5d9)

# Conflicts:
#	erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
2022-07-12 12:56:41 +00:00
Labeeb Mattra
d81e7e2421 Add NPA checkbox in Loan
(cherry picked from commit 27a8e16b28)
2022-07-12 12:56:41 +00:00
Labeeb Mattra
8181d61293 Update list view for Accrual and Shortfall
(cherry picked from commit 7d6e4898c4)
2022-07-12 12:56:40 +00:00
Labeeb Mattra
548aac5b11 Remove loan account field from doctype
(cherry picked from commit 6febcd529b)
2022-07-12 12:56:40 +00:00
Labeeb Mattra
de69fee6aa Use new adjustment amount fields
(cherry picked from commit 7d468a8778)
2022-07-12 12:56:39 +00:00
Labeeb Mattra
c3e6d7ba29 Seperate credit and debit adjust amount fields in Loan
(cherry picked from commit d6f632a770)
2022-07-12 12:56:38 +00:00
Abhinav Raut
d5ce780e87 feat: add adjustment amount to loan
- fix: bugs in loan balance adjustment

(cherry picked from commit 5c0a25012c)
2022-07-12 12:56:38 +00:00
Labeeb Mattra
a2125e694d Add Loan Balance Adjustment doctype
(cherry picked from commit 2e8f056514)
2022-07-12 12:56:37 +00:00
Labeeb Mattra
0d94653c5b Add Loan Refund doctype
(cherry picked from commit e1682965c5)
2022-07-12 12:56:36 +00:00
Labeeb Mattra
d9723a12c4 Add refund amount to loan
(cherry picked from commit 88cd780ca1)
2022-07-12 12:56:36 +00:00
Labeeb Mattra
d24cb01a9d Add more loan interest accrual types
(cherry picked from commit a81da2ea85)
2022-07-12 12:56:35 +00:00
Labeeb Mattra
ab214bcdfc update loan interest accrual types
(cherry picked from commit 900c878e03)
2022-07-12 12:56:35 +00:00
Frappe PR Bot
751fbd6794 chore(release): Bumped to Version 13.35.2
## [13.35.2](https://github.com/frappe/erpnext/compare/v13.35.1...v13.35.2) (2022-07-12)

### Bug Fixes

* components in the same table don't get updated value of prev payment-days based component ([01beb6f](01beb6f391))
* conflicts ([2045df1](2045df19f9))
* Incorrect provisional expense booking while reposting ([d182137](d182137ed1))
* timeout error while reposting ([07b80c2](07b80c295d))
* Use Contact Name instead of Supplier in RFQ Email ([b0e17de](b0e17dea2a))
* Use fallback conversion factor while setting incoming rate for petty purchase ([2a432c2](2a432c22d4))
* Validate payment-days-based dependent component ([a28c7cf](a28c7cf094))
2022-07-12 09:29:26 +00:00
Deepesh Garg
36566c1d14 Merge pull request #31574 from frappe/version-13-hotfix
chore: weekly version-13 release
2022-07-12 14:57:39 +05:30
Rucha Mahabal
28e4e4320e Merge pull request #31521 from ruchamahabal/fix-salary-calc-pd
fix: components in the same table don't get updated value of prev payment-days based component
2022-07-12 12:06:21 +05:30
Rucha Mahabal
1133062bfc Merge branch 'version-13-hotfix' into fix-salary-calc-pd 2022-07-12 11:42:07 +05:30
Rucha Mahabal
a28c7cf094 fix: Validate payment-days-based dependent component 2022-07-12 11:39:00 +05:30
Deepesh Garg
1c686c732e chore: fix query
(cherry picked from commit e04e67c6bf)
2022-07-11 15:44:49 +00:00
Deepesh Garg
66c5290dee chore: Ignore validation
(cherry picked from commit 3cf609fab1)
2022-07-11 15:44:49 +00:00
Deepesh Garg
9ac9c46dab fix: Allow multi currency invoice against single party account
(cherry picked from commit c83fbd5c50)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2022-07-11 15:44:48 +00:00
Sagar Sharma
faa489bc73 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-31412 2022-07-11 17:11:21 +05:30
Sagar Sharma
b17850ac3e chore: conflicts 2022-07-11 17:09:30 +05:30
Marica
7d36bdf292 Merge pull request #31565 from frappe/mergify/bp/version-13-hotfix/pr-31550
fix: Use Contact Name instead of Supplier in RFQ Email (backport #31550)
2022-07-11 12:35:18 +05:30
marination
b331c462ef chore: Instantiate variable unconditionally
(cherry picked from commit 300e812a1f)
2022-07-11 05:52:08 +00:00
marination
b0e17dea2a fix: Use Contact Name instead of Supplier in RFQ Email
(cherry picked from commit 88ac519b24)
2022-07-11 05:52:07 +00:00
Deepesh Garg
6d04bafb04 Merge pull request #31518 from frappe/mergify/bp/version-13-hotfix/pr-31516
fix: Incorrect provisional expense booking while reposting (backport #31516)
2022-07-07 11:53:58 +05:30
rohitwaghchaure
c4f39c7b8b Merge pull request #31538 from frappe/mergify/bp/version-13-hotfix/pr-31519
fix: timeout error while reposting (backport #31519)
2022-07-06 14:33:26 +05:30
rohitwaghchaure
2045df19f9 fix: conflicts 2022-07-06 12:34:10 +05:30
Rohit Waghchaure
07b80c295d fix: timeout error while reposting
(cherry picked from commit 78c8bb251e)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
2022-07-06 07:02:31 +00:00
Marica
a8c3882400 Merge pull request #31534 from frappe/mergify/bp/version-13-hotfix/pr-31529
fix: Use fallback conversion factor while setting incoming rate for petty purchase (backport #31529)
2022-07-06 01:20:51 +05:30
marination
2a432c22d4 fix: Use fallback conversion factor while setting incoming rate for petty purchase
- PIs for petty items (that do not need an Item record) are allowed using Item Name field
- If a different UOM is used in this case, conversion factor stays 0 and causes an error
- Fallback to 1 in `set_incoming_rate` for buying
- Selling will need a proper item, so this change is not needed there

(cherry picked from commit aa043fe961)
2022-07-05 19:09:36 +00:00
Deepesh Garg
776e807ade Merge pull request #31530 from vorasmit/warn-remove-india
chore: deprecation warning for remove-india
2022-07-05 22:46:33 +05:30
Smit Vora
6bda2a0865 chore: deprecation warning for remove-india 2022-07-05 21:20:51 +05:30
Rucha Mahabal
01beb6f391 fix: components in the same table don't get updated value of prev payment-days based component 2022-07-04 23:14:26 +05:30
Deepesh Garg
d182137ed1 fix: Incorrect provisional expense booking while reposting
(cherry picked from commit 60aad31162)
2022-07-04 14:58:41 +00:00
Deepesh Garg
fecf567e92 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-31412 2022-06-30 16:46:59 +05:30
Sagar Sharma
96bd493a6f Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-31412 2022-06-24 13:17:43 +05:30
s-aga-r
def622c13e fix: set default_bom for item
(cherry picked from commit dc2830da4d)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/test_bom.py
2022-06-21 11:13:31 +00:00
65 changed files with 1710 additions and 323 deletions

View File

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

View File

@@ -19,6 +19,7 @@
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"post_change_gl_entries",
"enable_discount_accounting",
"tax_settings_section",
@@ -276,14 +277,21 @@
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"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"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-11 17:42:36.427699",
"modified": "2022-07-11 13:37:50.605141",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -43,7 +43,7 @@ class GLEntry(Document):
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account()
@@ -52,7 +52,7 @@ class GLEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()

View File

@@ -25,7 +25,10 @@ from erpnext.accounts.utils import (
get_stock_and_account_balance,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.expense_claim.expense_claim import (
get_outstanding_amount_for_claim,
update_reimbursed_amount,
)
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -935,15 +938,12 @@ class JournalEntry(AccountsController):
def validate_expense_claim(self):
for d in self.accounts:
if d.reference_type == "Expense Claim":
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
)
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:
outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
if d.debit > outstanding_amt:
frappe.throw(
_(
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
).format(d.idx, d.reference_name, pending_amount)
"Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}"
).format(d.idx, d.reference_name, outstanding_amt)
)
def validate_credit_debit_note(self):

View File

@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
callback: function(r) {
if (r.message.length == 1) {
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
} else if (r.message.length < 50) {
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
}
}
});
});

View File

@@ -30,7 +30,10 @@ from erpnext.controllers.accounts_controller import (
get_supplier_block_status,
validate_taxes_and_charges,
)
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.expense_claim.expense_claim import (
get_outstanding_amount_for_claim,
update_reimbursed_amount,
)
from erpnext.setup.utils import get_exchange_rate
@@ -1649,12 +1652,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = (
flt(ref_doc.get("total_sanctioned_amount"))
+ flt(ref_doc.get("total_taxes_and_charges"))
- flt(ref_doc.get("total_amount_reimbursed"))
- flt(ref_doc.get("total_advance_amount"))
)
outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
elif reference_doctype == "Employee Advance":
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:

View File

@@ -10,10 +10,11 @@
"fiscal_year",
"amended_from",
"company",
"cost_center_wise_pnl",
"column_break1",
"closing_account_head",
"remarks"
"remarks",
"gle_processing_status",
"error_message"
],
"fields": [
{
@@ -86,17 +87,26 @@
"reqd": 1
},
{
"default": "0",
"fieldname": "cost_center_wise_pnl",
"fieldtype": "Check",
"label": "Book Cost Center Wise Profit/Loss"
"depends_on": "eval:doc.docstatus!=0",
"fieldname": "gle_processing_status",
"fieldtype": "Select",
"label": "GL Entry Processing Status",
"options": "In Progress\nCompleted\nFailed",
"read_only": 1
},
{
"depends_on": "eval:doc.gle_processing_status=='Failed'",
"fieldname": "error_message",
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-20 15:27:37.210458",
"modified": "2022-07-20 14:51:04.714154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",

View File

@@ -8,7 +8,6 @@ from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimensions,
)
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
@@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
self.validate_posting_date()
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
def on_cancel(self):
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
from erpnext.accounts.general_ledger import make_reverse_gl_entries
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
)
if gle_count > 5000:
frappe.enqueue(
make_reverse_gl_entries,
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
)
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
@@ -67,90 +81,80 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = []
pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
# pl account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
if flt(acc.bal_in_company_currency):
gl_entries.append(
self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
},
item=acc,
)
)
gl_entries.append(self.get_gle_for_pl_account(acc))
if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
gl_entries += gle_for_net_pl_bal
# closing liability account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc))
return gl_entries
def get_pnl_gl_entry(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
},
item=acc,
)
def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
self.update_default_dimensions(gl_entry)
gl_entries.append(gl_entry)
return gl_entries
def update_default_dimensions(self, gl_entry):
def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
gl_entry.update({dimension: acc.get(dimension)})
def get_pl_balances(self):
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
"""Get balance for dimension-wise pl accounts"""
dimension_fields = ["t1.cost_center", "t1.finance_book"]
@@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions:
dimension_fields.append("t1.{0}".format(dimension))
if group_by_account:
dimension_fields.append("t1.account")
return frappe.db.sql(
"""
select
t1.account, t2.account_currency, {dimension_fields},
t2.account_currency,
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s
group by t1.account, {dimension_fields}
where
t1.is_cancelled = 0
and t1.account = t2.name
and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2
and t2.company = %s
and t1.posting_date between %s and %s
group by {dimension_fields}
""".format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
)
def process_gl_entries(gl_entries):
from erpnext.accounts.general_ledger import make_gl_entries
try:
make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
try:
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")

View File

@@ -49,7 +49,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = (
("Cost of Goods Sold - TPC", 0.0, 600.0),
(surplus_account, 600.0, 400.0),
(surplus_account, 200.0, 0.0),
("Sales - TPC", 400.0, 0.0),
)
@@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
""",
(pcv.name),
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
@@ -94,7 +95,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
)
pcv = self.make_period_closing_voucher(submit=False)
pcv.cost_center_wise_pnl = 1
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
@@ -117,6 +117,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(pcv_gle, expected_gle)
pcv.reload()
pcv.cancel()
self.assertFalse(
frappe.db.get_value(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0},
)
)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")

View File

@@ -2623,20 +2623,34 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_discounts(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", False)
# Normal Itemized Discount
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = ""
si.items[0].discount_amount = 4000
si.items[1].discount_amount = 300
si.items[0].price_list_rate = 12
si.items[0].discount_percentage = 16.6666666667
si.items[0].rate = 10
si.items[1].price_list_rate = 15
si.items[1].discount_amount = 5
si.items[1].rate = 10
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 300)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 2100)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 222)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
# Invoice Discount on net total
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Net Total"
@@ -2646,10 +2660,17 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 253.61)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 66.57)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 243.11)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5613.71)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
# Invoice Discount on grand total (Itemized Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
@@ -2659,10 +2680,17 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 214.93)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 56.42)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 239.89)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5604.75)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
# Invoice Discount on grand total (Cash/Non-Trade Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
@@ -2675,8 +2703,107 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 222.0)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555.0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
def test_einvoice_without_discounts(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", True)
# Normal Itemized Discount
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = ""
si.items[0].price_list_rate = 12
si.items[0].discount_percentage = 16.6666666667
si.items[0].rate = 10
si.items[1].price_list_rate = 15
si.items[1].discount_amount = 5
si.items[1].rate = 10
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 10)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 10)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
# Invoice Discount on net total
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Net Total"
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.87)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.84)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.81)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.95)
# Invoice Discount on grand total (Itemized Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.89)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.87)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.84)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.96)
# Invoice Discount on grand total (Cash/Non-Trade Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
si.is_cash_or_non_trade_discount = 1
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
@@ -3276,6 +3403,36 @@ def get_sales_invoice_for_e_invoice():
},
)
si.append(
"items",
{
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 111,
"price_list_rate": 20,
"discount_percentage": 10,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
si.append(
"items",
{
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 1111,
"price_list_rate": 10,
"rate": 5,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
return si

View File

@@ -148,6 +148,7 @@ def update_net_values(entry):
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
for entry in gl_map:
# if there is already an entry in this account then just add it
# to that entry
@@ -229,9 +230,10 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.from_repost = from_repost
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False
gle.submit()
if not from_repost:
if not from_repost and gle.voucher_type != "Period Closing Voucher":
validate_expense_against_budget(args)

View File

@@ -8,11 +8,11 @@ from frappe import _
def execute(filters=None):
validate_filters(filters)
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
return columns, res
@@ -22,10 +22,11 @@ def validate_filters(filters):
frappe.throw(_("From Date must be before To Date"))
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map):
supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
print(journal_entry_party_map)
out = []
for name, details in gle_map.items():
@@ -38,6 +39,11 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
suppliers = journal_entry_party_map.get(name)
if suppliers:
supplier = suppliers[0]
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
@@ -176,6 +182,7 @@ def get_tds_docs(filters):
journal_entries = []
tax_category_map = {}
or_filters = {}
journal_entry_party_map = {}
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
tds_accounts = frappe.get_all(
@@ -218,9 +225,24 @@ def get_tds_docs(filters):
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_tax_category_map(journal_entries, "Journal Entry", tax_category_map)
return tds_documents, tds_accounts, tax_category_map
return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
journal_entry_party_map[d.parent] = []
journal_entry_party_map[d.parent].append(d.party)
return journal_entry_party_map
def get_tax_category_map(vouchers, doctype, tax_category_map):

View File

@@ -38,7 +38,6 @@
"purchase_date",
"section_break_23",
"calculate_depreciation",
"allow_monthly_depreciation",
"column_break_33",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
@@ -454,13 +453,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "calculate_depreciation",
"fieldname": "allow_monthly_depreciation",
"fieldtype": "Check",
"label": "Allow Monthly Depreciation"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_existing_asset",
@@ -503,7 +495,7 @@
"link_fieldname": "asset"
}
],
"modified": "2022-01-30 20:19:24.680027",
"modified": "2022-07-20 16:22:44.437579",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -545,4 +537,4 @@
"sort_order": "DESC",
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -353,61 +353,16 @@ class Asset(AccountsController):
skip_row = True
if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date
if self.allow_monthly_depreciation:
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
):
month_range = months
else:
month_range = finance_book.frequency_of_depreciation
for r in range(month_range):
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
# For first entry of monthly depr
if r == 0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
per_day_amt = depreciation_amount / days
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date
amount = depreciation_amount_for_current_month
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / (month_range - 1)
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(
month_range
) - 1:
# For last entry of monthly depr
date = last_schedule_date
amount = depreciation_amount / month_range
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range
self.append(
"schedules",
{
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
else:
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
@@ -853,7 +808,7 @@ class Asset(AccountsController):
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
return 100 * (1 - flt(depreciation_rate, float_precision))
return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)

View File

@@ -145,6 +145,7 @@ class TestAsset(AssetSetup):
def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset=1)
doc = frappe.new_doc("Purchase Invoice")
doc.company = "_Test Company"
doc.supplier = "_Test Supplier"
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
@@ -702,6 +703,8 @@ class TestDepreciationMethods(AssetSetup):
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
def test_monthly_depreciation_by_wdv_method(self):
existing_precision = frappe.db.get_default("float_precision")
frappe.db.set_default("float_precision", 3)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2022-02-15",
@@ -715,12 +718,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2022-02-28", 645.0, 645.0],
["2022-03-31", 1206.8, 1851.8],
["2022-04-30", 1051.12, 2902.92],
["2022-05-31", 915.52, 3818.44],
["2022-06-30", 797.42, 4615.86],
["2022-07-15", 384.14, 5000.0],
["2022-02-28", 647.25, 647.25],
["2022-03-31", 1210.71, 1857.96],
["2022-04-30", 1053.99, 2911.95],
["2022-05-31", 917.55, 3829.5],
["2022-06-30", 798.77, 4628.27],
["2022-07-15", 371.73, 5000.0],
]
schedules = [
@@ -731,8 +734,8 @@ class TestDepreciationMethods(AssetSetup):
]
for d in asset.get("schedules")
]
self.assertEqual(schedules, expected_schedules)
frappe.db.set_default("float_precision", existing_precision)
class TestDepreciationBasics(AssetSetup):

View File

@@ -181,12 +181,20 @@ class RequestforQuotation(BuyingController):
doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
# Get Contact Full Name
supplier_name = None
if data.get("contact"):
contact_name = frappe.db.get_value(
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
)
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
args = {
"update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link,
"user_fullname": full_name,
"supplier_name": data.get("supplier_name"),
"supplier_name": supplier_name or data.get("supplier_name"),
"supplier_salutation": self.salutation or "Dear Mx.",
}

View File

@@ -1470,8 +1470,15 @@ class AccountsController(TransactionBase):
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
)
party_account_currency = get_account_currency(party_account)
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
)
if not party_gle_currency and (party_account_currency != self.currency):
if (
not party_gle_currency
and (party_account_currency != self.currency)
and not allow_multi_currency_invoices_against_single_party_account
):
frappe.throw(
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
frappe.bold(party_account), party_account_currency, self.currency

View File

@@ -355,6 +355,8 @@ class Subcontracting:
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
rm_obj.amount = flt(rm_obj.required_qty) * flt(rm_obj.rate)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)

View File

@@ -483,12 +483,12 @@ scheduler_events = {
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
],
"daily": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",

View File

@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2021-06-17 11:31:37.730760",
"modified": "2022-07-18 20:03:43.188705",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@@ -349,7 +349,9 @@ def get_employee_email(employee_doc):
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
holiday_list, company = frappe.get_cached_value(
"Employee", employee, ["holiday_list", "company"]
)
else:
holiday_list = ""
company = frappe.db.get_value("Global Defaults", None, "default_company")

View File

@@ -26,7 +26,8 @@
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
@@ -48,7 +49,8 @@
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_4",
@@ -107,7 +109,7 @@
}
],
"links": [],
"modified": "2020-07-08 11:02:32.660986",
"modified": "2022-07-19 15:38:41.767539",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",

View File

@@ -131,7 +131,7 @@ def mark_attendance_and_link_log(
return None
elif attendance_status in ("Present", "Absent", "Half Day"):
employee_doc = frappe.get_doc("Employee", employee)
company = frappe.get_cached_value("Employee", employee, "company")
duplicate = frappe.db.exists(
"Attendance",
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
@@ -144,7 +144,7 @@ def mark_attendance_and_link_log(
"attendance_date": attendance_date,
"status": attendance_status,
"working_hours": working_hours,
"company": employee_doc.company,
"company": company,
"shift": shift,
"late_entry": late_entry,
"early_exit": early_exit,

View File

@@ -339,6 +339,30 @@ def update_reimbursed_amount(doc, amount):
frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
def get_outstanding_amount_for_claim(claim):
if isinstance(claim, str):
claim = frappe.db.get_value(
"Expense Claim",
claim,
(
"total_sanctioned_amount",
"total_taxes_and_charges",
"total_amount_reimbursed",
"total_advance_amount",
),
as_dict=True,
)
outstanding_amt = (
flt(claim.total_sanctioned_amount)
+ flt(claim.total_taxes_and_charges)
- flt(claim.total_amount_reimbursed)
- flt(claim.total_advance_amount)
)
return outstanding_amt
@frappe.whitelist()
def make_bank_entry(dt, dn):
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
@@ -348,11 +372,7 @@ def make_bank_entry(dt, dn):
if not default_bank_cash_account:
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
payable_amount = (
flt(expense_claim.total_sanctioned_amount)
- flt(expense_claim.total_amount_reimbursed)
- flt(expense_claim.total_advance_amount)
)
payable_amount = get_outstanding_amount_for_claim(expense_claim)
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Bank Entry"

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate, random_string
from erpnext.accounts.doctype.account.test_account import create_account
@@ -14,9 +15,18 @@ test_dependencies = ["Employee"]
company_name = "_Test Company 3"
class TestExpenseClaim(unittest.TestCase):
def tearDown(self):
frappe.db.rollback()
class TestExpenseClaim(FrappeTestCase):
def setUp(self):
if not frappe.db.get_value("Cost Center", {"company": company_name}):
frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Cost Center 3",
"parent_cost_center": "_Test Company 3 - _TC3",
"is_group": 0,
"company": company_name,
}
).insert()
def test_total_expense_claim_for_project(self):
frappe.db.sql("""delete from `tabTask`""")
@@ -58,12 +68,7 @@ class TestExpenseClaim(unittest.TestCase):
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
)
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
je.posting_date = nowdate()
je.cheque_no = random_string(5)
je.cheque_date = nowdate()
je.submit()
je = make_journal_entry(expense_claim)
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
self.assertEqual(expense_claim.status, "Paid")
@@ -272,6 +277,24 @@ class TestExpenseClaim(unittest.TestCase):
self.assertEqual(outstanding_amount, 0)
self.assertEqual(total_amount_reimbursed, 5500)
def test_journal_entry_against_expense_claim(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
expense_claim = make_expense_claim(
payable_account,
300,
200,
company_name,
"Travel Expenses - _TC3",
do_not_submit=True,
taxes=taxes,
)
expense_claim.submit()
je = make_journal_entry(expense_claim)
self.assertEqual(je.accounts[0].debit_in_account_currency, expense_claim.grand_total)
def get_payable_account(company):
return frappe.get_cached_value("Company", company, "default_payable_account")
@@ -370,3 +393,14 @@ def make_payment_entry(expense_claim, payable_account, amt):
pe.references[0].allocated_amount = amt
pe.insert()
pe.submit()
def make_journal_entry(expense_claim):
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
je.posting_date = nowdate()
je.cheque_no = random_string(5)
je.cheque_date = nowdate()
je.submit()
return je

View File

@@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
if date is None:
date = today()
if holiday_list:
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
return bool(
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
)
else:
return False

View File

@@ -25,7 +25,8 @@
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
@@ -48,7 +49,8 @@
"in_list_view": 1,
"label": "Shift Type",
"options": "Shift Type",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_3",
@@ -105,7 +107,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-15 14:27:54.310773",
"modified": "2022-07-19 15:27:54.310773",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Assignment",

View File

@@ -169,7 +169,7 @@ def get_employee_shift(
"""
if for_date is None:
for_date = nowdate()
default_shift = frappe.db.get_value("Employee", employee, "default_shift")
default_shift = frappe.get_cached_value("Employee", employee, "default_shift")
shift_type_name = None
shift_assignment_details = frappe.db.get_value(
"Shift Assignment",
@@ -187,7 +187,7 @@ def get_employee_shift(
if not shift_type_name and consider_default_shift:
shift_type_name = default_shift
if shift_type_name:
holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
holiday_list_name = frappe.get_cached_value("Shift Type", shift_type_name, "holiday_list")
if not holiday_list_name:
holiday_list_name = get_holiday_list_for_employee(employee, False)
if holiday_list_name and is_holiday(holiday_list_name, for_date):
@@ -294,7 +294,18 @@ def get_shift_details(shift_type_name, for_date=None):
return None
if not for_date:
for_date = nowdate()
shift_type = frappe.get_doc("Shift Type", shift_type_name)
shift_type = frappe.get_cached_value(
"Shift Type",
shift_type_name,
[
"name",
"start_time",
"end_time",
"begin_check_in_before_shift_start_time",
"allow_check_out_after_shift_end_time",
],
as_dict=1,
)
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
for_date = (
for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date

View File

@@ -107,7 +107,7 @@ class ShiftType(Document):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
The Absent is marked starting from 'process_attendance_after' or employee creation date.
"""
date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
date_of_joining, relieving_date, employee_creation = frappe.get_cached_value(
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
)
if not date_of_joining:
@@ -156,21 +156,19 @@ class ShiftType(Document):
if not from_date:
del filters["start_date"]
assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
assigned_employees = [x[0] for x in assigned_employees]
assigned_employees = frappe.get_all("Shift Assignment", filters, pluck="employee")
if consider_default_shift:
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
default_shift_employees = [x[0] for x in default_shift_employees]
default_shift_employees = frappe.get_all("Employee", filters, pluck="name")
return list(set(assigned_employees + default_shift_employees))
return assigned_employees
def process_auto_attendance_for_all_shifts():
shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
shift_list = frappe.get_all("Shift Type", filters={"enable_auto_attendance": "1"}, pluck="name")
for shift in shift_list:
doc = frappe.get_doc("Shift Type", shift[0])
doc = frappe.get_cached_doc("Shift Type", shift)
doc.process_auto_attendance()

View File

@@ -48,6 +48,10 @@
"total_payment",
"total_principal_paid",
"written_off_amount",
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
"is_npa",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@@ -379,12 +383,39 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "credit_adjustment_amount",
"fieldtype": "Currency",
"label": "Credit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "debit_adjustment_amount",
"fieldtype": "Currency",
"label": "Debit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"default": "0",
"description": "Mark Loan as a Nonperforming asset",
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-10 11:50:31.957360",
"modified": "2022-06-30 12:04:13.728880",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Balance Adjustment', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,189 @@
{
"actions": [],
"autoname": "LM-ADJ-.#####",
"creation": "2022-06-28 14:48:47.736269",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"adjustment_account",
"column_break_11",
"adjustment_type",
"amount",
"reference_number",
"remarks",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Adjustment Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "adjustment_account",
"fieldtype": "Link",
"label": "Adjustment Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "adjustment_type",
"fieldtype": "Select",
"label": "Adjustment Type",
"options": "Credit Adjustment\nDebit Adjustment",
"reqd": 1
},
{
"fieldname": "remarks",
"fieldtype": "Data",
"label": "Remarks"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-07-08 16:48:54.480066",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Balance Adjustment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,143 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, nowdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanBalanceAdjustment(AccountsController):
"""
Add credit/debit adjustments to loan ledger.
"""
def validate(self):
if self.amount == 0:
frappe.throw(_("Amount cannot be zero"))
if self.amount < 0:
frappe.throw(_("Amount cannot be negative"))
self.set_missing_values()
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.posting_date:
self.posting_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.db.get_value(
"Loan",
self.loan,
[
"loan_amount",
"credit_adjustment_amount",
"debit_adjustment_amount",
"total_payment",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
],
as_dict=1,
)
if cancel:
adjustment_amount = self.get_values_on_cancel(loan_details)
else:
adjustment_amount = self.get_values_on_submit(loan_details)
if self.adjustment_type == "Credit Adjustment":
adj_field = "credit_adjustment_amount"
elif self.adjustment_type == "Debit Adjustment":
adj_field = "debit_adjustment_amount"
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
def get_values_on_cancel(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
return adjustment_amount
def get_values_on_submit(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(
posting_date=add_days(self.posting_date, -1),
loan=self.loan,
accrual_type=self.adjustment_type,
)
return adjustment_amount
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
loan_entry = {
"account": loan_account,
"against": self.adjustment_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.posting_date,
}
company_entry = {
"account": self.adjustment_account,
"against": loan_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": self.posting_date,
}
if self.adjustment_type == "Credit Adjustment":
loan_entry["credit"] = self.amount
loan_entry["credit_in_account_currency"] = self.amount
company_entry["debit"] = self.amount
company_entry["debit_in_account_currency"] = self.amount
elif self.adjustment_type == "Debit Adjustment":
loan_entry["debit"] = self.amount
loan_entry["debit_in_account_currency"] = self.amount
company_entry["credit"] = self.amount
company_entry["credit_in_account_currency"] = self.amount
gle_map.append(self.get_gl_dict(loan_entry))
gle_map.append(self.get_gl_dict(company_entry))
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanBalanceAdjustment(FrappeTestCase):
pass

View File

@@ -35,12 +35,15 @@
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan",
"options": "Loan"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date"
},
{
@@ -75,6 +78,8 @@
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
},
@@ -158,8 +163,11 @@
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
},
{
"fieldname": "penalty_amount",
@@ -185,10 +193,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:26:38.871889",
"modified": "2022-06-30 11:51:31.911794",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -225,5 +234,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Refund', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,176 @@
{
"actions": [],
"autoname": "LM-RF-.#####",
"creation": "2022-06-24 15:51:03.165498",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"refund_account",
"column_break_11",
"refund_amount",
"reference_number",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Refund Details"
},
{
"fieldname": "refund_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Refund Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-06-24 16:13:48.793486",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Refund",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanRefund(AccountsController):
"""
Add refund if total repayment is more than that is owed.
"""
def validate(self):
self.set_missing_values()
self.validate_refund_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_refund_amount(self):
loan = frappe.get_doc("Loan", self.loan)
pending_amount = get_pending_principal_amount(loan)
if pending_amount >= 0:
frappe.throw(_("No excess amount to refund."))
else:
excess_amount = pending_amount * -1
if self.refund_amount > excess_amount:
frappe.throw(_("Refund amount cannot be greater than excess amount {0}").format(excess_amount))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
refund_amount = frappe.db.get_value("Loan", self.loan, "refund_amount")
if cancel:
refund_amount -= self.refund_amount
else:
refund_amount += self.refund_amount
frappe.db.set_value("Loan", self.loan, "refund_amount", refund_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict(
{
"account": self.refund_account,
"against": loan_details.loan_account,
"credit": self.refund_amount,
"credit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.refund_account,
"debit": self.refund_amount,
"debit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanRefund(FrappeTestCase):
pass

View File

@@ -387,15 +387,19 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
self.shortfall_amount, self.against_loan
)
elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
remarks = "Shortfall repayment of {0} against loan {1}".format(
self.shortfall_amount, self.against_loan
)
else:
remarks = _("Repayment against Loan: ") + self.against_loan
remarks = "Repayment against loan " + self.against_loan
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
if self.repay_from_salary:
payment_account = self.payroll_payable_account
@@ -446,7 +450,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
@@ -464,7 +468,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
@@ -624,16 +628,22 @@ def get_pending_principal_amount(loan):
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
pending_principal_amount = (
flt(loan.total_payment)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
else:
pending_principal_amount = (
flt(loan.disbursed_amount)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
return pending_principal_amount

View File

@@ -7,6 +7,8 @@
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"status",
"column_break_3",
"shortfall_time",
@@ -23,6 +25,8 @@
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan ",
"options": "Loan",
"read_only": 1
@@ -91,17 +95,35 @@
{
"fieldname": "shortfall_percentage",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Shortfall Percentage",
"read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-01 08:13:43.263772",
"modified": "2022-06-30 11:57:09.378089",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Shortfall",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -132,5 +154,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -54,17 +54,18 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement",
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-06 13:28:51.478909",
"modified": "2022-06-29 11:19:33.203088",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -98,5 +99,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -444,6 +444,7 @@ class BOM(WebsiteGenerator):
and self.is_active
):
frappe.db.set(self, "is_default", 1)
frappe.db.set_value("Item", self.item, "default_bom", self.name)
else:
frappe.db.set(self, "is_default", 0)
item = frappe.get_doc("Item", self.item)

View File

@@ -519,6 +519,42 @@ class TestBOM(FrappeTestCase):
new_bom.delete()
def test_set_default_bom_for_item_having_single_bom(self):
from erpnext.stock.doctype.item.test_item import make_item
fg_item = make_item(properties={"is_stock_item": 1})
bom_item = make_item(properties={"is_stock_item": 1})
# Step 1: Create BOM
bom = frappe.new_doc("BOM")
bom.item = fg_item.item_code
bom.quantity = 1
bom.append(
"items",
{
"item_code": bom_item.item_code,
"qty": 1,
"uom": bom_item.stock_uom,
"stock_uom": bom_item.stock_uom,
"rate": 100.0,
},
)
bom.save()
bom.submit()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
# Step 2: Uncheck is_active field
bom.is_active = 0
bom.save()
bom.reload()
self.assertIsNone(frappe.get_value("Item", fg_item.item_code, "default_bom"))
# Step 3: Check is_active field
bom.is_active = 1
bom.save()
bom.reload()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -368,3 +368,6 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
erpnext.patches.v13_0.update_employee_advance_status
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v13_0.add_cost_center_in_loans
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v13_0.reset_corrupt_defaults

View File

@@ -33,7 +33,10 @@ def execute():
"insert_after": insert_after_field,
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
try:
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
except Exception as e:
pass
count += 1

View File

@@ -0,0 +1,16 @@
import frappe
def execute():
if not frappe.db.has_column("Asset", "allow_monthly_depreciation"):
return
assets = frappe.get_all("Asset", filters={"allow_monthly_depreciation": 1})
for d in assets:
print(d.name)
asset_doc = frappe.get_doc("Asset", d.name)
for i in asset_doc.get("finance_books"):
if i.frequency_of_depreciation != 1:
i.total_number_of_depreciations *= i.frequency_of_depreciation
i.frequency_of_depreciation = 1
i.db_update()

View File

@@ -0,0 +1,29 @@
import frappe
def execute():
"""
This patch is needed to fix parent incorrectly set as `__2fa` because of
https://github.com/frappe/frappe/commit/a822092211533ff17ff9b92dd86f6f868ed63e2e
"""
for doctype in (
"Accounts Settings",
"Stock Settings",
"Selling Settings",
"Buying Settings",
"CRM Settings",
"Global Defaults",
"Healthcare Settings",
"Education Settings",
):
try:
frappe.get_single(doctype).save()
except Exception:
pass
try:
pos_profile = frappe.get_last_doc("POS Profile")
pos_profile.set_defaults()
except Exception:
pass

View File

@@ -0,0 +1,15 @@
import click
import frappe
def execute():
if not frappe.db.exists("Company", {"country": "India"}):
return
click.secho(
"India-specific regional features have been moved to a separate app"
" and will be removed from ERPNext in Version 14."
" Please install India Compliance after upgrading to Version 14:\n"
"https://github.com/resilient-tech/india-compliance",
fg="yellow",
)

View File

@@ -626,7 +626,7 @@ class SalarySlip(TransactionBase):
for struct_row in self._salary_structure_doc.get(component_type):
amount = self.eval_condition_and_formula(struct_row, data)
if amount is not None and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, component_type)
self.update_component_row(struct_row, amount, component_type, data=data)
def get_data_for_eval(self):
"""Returns data for evaluating formula"""
@@ -780,7 +780,7 @@ class SalarySlip(TransactionBase):
self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(
self, component_data, amount, component_type, additional_salary=None, is_recurring=0
self, component_data, amount, component_type, additional_salary=None, is_recurring=0, data=None
):
component_row = None
for d in self.get(component_type):
@@ -850,6 +850,8 @@ class SalarySlip(TransactionBase):
component_row.amount = amount
self.update_component_amount_based_on_payment_days(component_row)
if data:
data[component_row.abbr] = component_row.amount
def update_component_amount_based_on_payment_days(self, component_row):
joining_date, relieving_date = self.get_joining_and_relieving_dates()

View File

@@ -1119,6 +1119,7 @@ def make_earning_salary_component(
"formula": "BS*.5",
"type": "Earning",
"amount_based_on_formula": 1,
"depends_on_payment_days": 0,
},
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
]

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import re
import frappe
from frappe import _
@@ -18,6 +19,7 @@ class SalaryStructure(Document):
self.strip_condition_and_formula_fields()
self.validate_max_benefits_with_flexi()
self.validate_component_based_on_tax_slab()
self.validate_payment_days_based_dependent_component()
def set_missing_values(self):
overwritten_fields = [
@@ -58,6 +60,35 @@ class SalaryStructure(Document):
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
frappe.throw(_("Net pay cannot be negative"))
def validate_payment_days_based_dependent_component(self):
abbreviations = self.get_component_abbreviations()
for component_type in ("earnings", "deductions"):
for row in self.get(component_type):
if (
row.formula
and row.depends_on_payment_days
# check if the formula contains any of the payment days components
and any(re.search(r"\b" + abbr + r"\b", row.formula) for abbr in abbreviations)
):
message = _("Row #{0}: The {1} Component has the options {2} and {3} enabled.").format(
row.idx,
frappe.bold(row.salary_component),
frappe.bold("Amount based on formula"),
frappe.bold("Depends On Payment Days"),
)
message += "<br><br>" + _(
"Disable {0} for the {1} component, to prevent the amount from being deducted twice, as its formula already uses a payment-days-based component."
).format(
frappe.bold("Depends On Payment Days"), frappe.bold(row.salary_component)
)
frappe.throw(message, title=_("Payment Days Dependency"))
def get_component_abbreviations(self):
abbr = [d.abbr for d in self.earnings if d.depends_on_payment_days]
abbr += [d.abbr for d in self.deductions if d.depends_on_payment_days]
return abbr
def strip_condition_and_formula_fields(self):
# remove whitespaces from condition and formula fields
for row in self.earnings:

View File

@@ -9,6 +9,9 @@
"section_break_2",
"sandbox_mode",
"applicable_from",
"column_break_4",
"dont_show_discounts_in_e_invoice",
"section_break_7",
"credentials",
"advanced_settings_section",
"client_id",
@@ -80,12 +83,28 @@
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"default": "1",
"description": "Enabling this will directly report net rates in e-Invoice post discounts",
"fieldname": "dont_show_discounts_in_e_invoice",
"fieldtype": "Check",
"label": "Don't show discounts in e-Invoice"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hide_border": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-11-16 19:50:28.029517",
"modified": "2022-07-17 14:57:50.783517",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",

View File

@@ -144,7 +144,7 @@ class GSTR3BReport(Document):
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(
"""
SELECT p.place_of_supply, p.supplier_address,
SELECT p.name, p.place_of_supply, p.supplier_address, p.gst_category,
i.base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent

View File

@@ -65,6 +65,7 @@ def validate_eligibility(doc):
"SEZ",
"Overseas",
"Deemed Export",
"UIN Holders",
]
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
@@ -130,9 +131,7 @@ def read_json(name):
def get_transaction_details(invoice):
supply_type = ""
if (
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
):
if invoice.gst_category in ("Registered Regular", "Registered Composition", "UIN Holders"):
supply_type = "B2B"
elif invoice.gst_category == "SEZ":
if invoice.export_type == "Without Payment of Tax":
@@ -148,15 +147,18 @@ def get_transaction_details(invoice):
supply_type = "DEXP"
if not supply_type:
rr, rc, sez, overseas, export = (
rr, rc, sez, overseas, export, uin = (
bold("Registered Regular"),
bold("Registered Composition"),
bold("SEZ"),
bold("Overseas"),
bold("Deemed Export"),
bold("UIN Holders"),
)
frappe.throw(
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
_("GST category should be one of {}, {}, {}, {}, {}, {}").format(
rr, rc, sez, overseas, export, uin
),
title=_("Invalid Supply Type"),
)
@@ -272,14 +274,43 @@ def get_item_list(invoice):
item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
item_qty = item.qty
if invoice.get("apply_discount_on"):
item.discount_amount = item.base_amount - item.base_net_amount
item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty
item.gross_amount = abs(item.taxable_value) + item.discount_amount
item.discount_amount = abs(item.discount_amount)
item.taxable_value = abs(item.taxable_value)
if invoice.get("is_return") or invoice.get("is_debit_note"):
item_qty = item_qty or 1
hide_discount_in_einvoice = cint(
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
)
if hide_discount_in_einvoice:
item.unit_rate = item.taxable_value / item_qty
item.gross_amount = item.taxable_value
item.discount_amount = 0
else:
if invoice.get("apply_discount_on") and (abs(invoice.get("base_discount_amount") or 0.0) > 0.0):
# TODO: need to handle case when tax included in basic rate is checked.
item.discount_amount = (item.discount_amount * item_qty) + (
abs(item.base_amount) - abs(item.base_net_amount)
)
else:
item.discount_amount = item.discount_amount * item_qty
try:
item.unit_rate = (item.taxable_value + item.discount_amount) / item_qty
except ZeroDivisionError:
# This will never run but added as safety measure
frappe.throw(
title=_("Error: Qty is Zero"),
msg=_("Quantity can't be zero unless it's Credit/Debit Note."),
)
item.gross_amount = item.taxable_value + item.discount_amount
item.taxable_value = item.taxable_value
item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
item.serial_no = ""

View File

@@ -714,7 +714,7 @@ def get_custom_fields():
insert_after="customer",
no_copy=1,
print_hide=1,
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export", "UIN Holders"], doc.gst_category) && doc.irn_cancelled === 0',
),
dict(
fieldname="irn_cancelled",

View File

@@ -40,13 +40,12 @@ def validate_gstin_for_india(doc, method):
gst_category = []
if hasattr(doc, "gst_category"):
if len(doc.links):
link_doctype = doc.links[0].get("link_doctype")
link_name = doc.links[0].get("link_name")
if len(doc.links):
link_doctype = doc.links[0].get("link_doctype")
link_name = doc.links[0].get("link_name")
if link_doctype in ["Customer", "Supplier"]:
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
if link_doctype in ["Customer", "Supplier"]:
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
doc.gstin = doc.gstin.upper().strip()
if not doc.gstin or doc.gstin == "NA":

View File

@@ -164,4 +164,13 @@ class TestItemPrice(FrappeTestCase):
self.assertEqual(price, 21)
def make_item_price(item_code, price_list, price_list_rate):
item_price = frappe.new_doc("Item Price")
item_price.price_list = price_list
item_price.item_code = item_code
item_price.price_list_rate = price_list_rate
item_price.insert()
return item_price
test_records = frappe.get_test_records("Item Price")

View File

@@ -4,7 +4,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now
from frappe.utils import add_days, add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -120,6 +120,61 @@ class TestLandedCostVoucher(FrappeTestCase):
expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}"
)
def test_landed_cost_voucher_stock_impact(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr1 = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=500,
rate=80,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
pr2 = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=100,
rate=80,
posting_date=frappe.utils.nowdate(),
)
last_sle = frappe.db.get_value( # SLE of second PR
"Stock Ledger Entry",
{
"voucher_type": pr2.doctype,
"voucher_no": pr2.name,
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"],
as_dict=1,
)
create_landed_cost_voucher("Purchase Receipt", pr1.name, pr1.company)
last_sle_after_landed_cost = frappe.db.get_value( # SLE of second PR after LCV's effect
"Stock Ledger Entry",
{
"voucher_type": pr2.doctype,
"voucher_no": pr2.name,
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"],
as_dict=1,
)
self.assertEqual(
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -219,11 +274,11 @@ class TestLandedCostVoucher(FrappeTestCase):
landed costs, this should be allowed for serial nos too.
Case:
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
"""
serial_no = "LCV_TEST_SR_NO"
item_code = "_Test Serialized Item"

View File

@@ -468,6 +468,7 @@ class PurchaseReceipt(BuyingController):
and not d.is_fixed_asset
and flt(d.qty)
and provisional_accounting_for_non_stock_items
and d.get("provisional_expense_account")
):
self.add_provisional_gl_entry(
d, gl_entries, self.posting_date, d.get("provisional_expense_account")

View File

@@ -5,7 +5,7 @@
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate, nowtime
from frappe.utils import add_days, flt, nowdate, nowtime
from six import iteritems
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -1414,6 +1414,138 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
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
# Inward at WH1 warehouse (Component)
# 1st Repack (Component (WH1) - Subcomponent (WH2))
# 2nd Repack (Subcomponent (WH2) - FG Item (WH3))
# Material Transfer of FG Item -> WH 3 -> WH2 -> Wh1 (Two transfer entries)
# Backdated transction which should update valuation rate in repack as well trasfer entries
for item_code in ["FG Item 1", "Sub Component 1", "Component 1"]:
create_item(item_code)
for warehouse in ["WH 1", "WH 2", "WH 3"]:
create_warehouse(warehouse)
make_stock_entry(
item_code="Component 1",
rate=100,
purpose="Material Receipt",
qty=10,
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -10),
)
repack1 = make_stock_entry(
item_code="Component 1",
purpose="Repack",
do_not_save=True,
qty=10,
from_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -9),
)
repack1.append(
"items",
{
"item_code": "Sub Component 1",
"qty": 10,
"t_warehouse": "WH 2 - _TC",
"transfer_qty": 10,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1.0,
},
)
repack1.save()
repack1.submit()
self.assertEqual(repack1.items[1].basic_rate, 100)
self.assertEqual(repack1.items[1].amount, 1000)
repack2 = make_stock_entry(
item_code="Sub Component 1",
purpose="Repack",
do_not_save=True,
qty=10,
from_warehouse="WH 2 - _TC",
posting_date=add_days(nowdate(), -8),
)
repack2.append(
"items",
{
"item_code": "FG Item 1",
"qty": 10,
"t_warehouse": "WH 3 - _TC",
"transfer_qty": 10,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1.0,
},
)
repack2.save()
repack2.submit()
self.assertEqual(repack2.items[1].basic_rate, 100)
self.assertEqual(repack2.items[1].amount, 1000)
transfer1 = make_stock_entry(
item_code="FG Item 1",
purpose="Material Transfer",
qty=10,
from_warehouse="WH 3 - _TC",
to_warehouse="WH 2 - _TC",
posting_date=add_days(nowdate(), -7),
)
self.assertEqual(transfer1.items[0].basic_rate, 100)
self.assertEqual(transfer1.items[0].amount, 1000)
transfer2 = make_stock_entry(
item_code="FG Item 1",
purpose="Material Transfer",
qty=10,
from_warehouse="WH 2 - _TC",
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -6),
)
self.assertEqual(transfer2.items[0].basic_rate, 100)
self.assertEqual(transfer2.items[0].amount, 1000)
# Backdated transaction
receipt2 = make_stock_entry(
item_code="Component 1",
rate=200,
purpose="Material Receipt",
qty=10,
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -15),
)
self.assertEqual(receipt2.items[0].basic_rate, 200)
self.assertEqual(receipt2.items[0].amount, 2000)
repost_name = frappe.db.get_value(
"Repost Item Valuation", {"voucher_no": receipt2.name, "docstatus": 1}, "name"
)
doc = frappe.get_doc("Repost Item Valuation", repost_name)
repost_sl_entries(doc)
for obj in [repack1, repack2, transfer1, transfer2]:
obj.load_from_db()
index = 1 if obj.purpose == "Repack" else 0
self.assertEqual(obj.items[index].basic_rate, 200)
self.assertEqual(obj.items[index].basic_amount, 2000)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -94,27 +94,26 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after(
{
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get("name"),
"creation": args.get("creation"),
},
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
if not (args.get("is_cancelled") and via_landed_cost_voucher):
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after(
{
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get("name"),
"creation": args.get("creation"),
},
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
# update qty in future sle and Validate negative qty
# For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
update_qty_in_future_sle(args, allow_negative_stock)
@@ -208,8 +207,14 @@ def repost_future_sle(
allow_negative_stock=None,
via_landed_cost_voucher=False,
):
if not args and voucher_type and voucher_no:
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
if not args:
args = [] # set args to empty list if None to avoid enumerate error
items_to_be_repost = get_items_to_be_repost(
voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
)
if items_to_be_repost:
args = items_to_be_repost
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
affected_transactions = get_affected_transactions(doc)
@@ -247,16 +252,11 @@ def repost_future_sle(
data.sle_changed = False
i += 1
if doc and i % 2 == 0:
if doc:
update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
if doc and args:
update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
def update_args_in_repost_item_valuation(
doc, index, args, distinct_item_warehouses, affected_transactions
@@ -281,17 +281,21 @@ def update_args_in_repost_item_valuation(
)
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
items_to_be_repost = []
if doc and doc.items_to_be_repost:
return json.loads(doc.items_to_be_repost) or []
items_to_be_repost = json.loads(doc.items_to_be_repost) or []
return frappe.db.get_all(
"Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse",
)
if not items_to_be_repost and voucher_type and voucher_no:
items_to_be_repost = frappe.db.get_all(
"Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse",
)
return items_to_be_repost
def get_distinct_item_warehouse(args=None, doc=None):
@@ -491,7 +495,9 @@ class update_entries_after(object):
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
else:
return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
self.initialize_previous_data(dependant_sle)
self.update_distinct_item_warehouses(dependant_sle)
return entries_to_fix
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
@@ -508,17 +514,6 @@ class update_entries_after(object):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
{"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
)
future_sle_for_dependant = list(self.get_sle_after_datetime(args))
entries_to_fix.extend(future_sle_for_dependant)
return sorted(entries_to_fix, key=lambda k: k["timestamp"])
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@@ -898,6 +898,7 @@ ERPNext Settings,ERPNext-Einstellungen,
Earliest,Frühestens,
Earnest Money,Anzahlung,
Earning,Einkommen,
Earnings & Deductions,Verdienste & Abzüge,
Edit,Bearbeiten,
Edit Publishing Details,Bearbeitungsdetails bearbeiten,
"Edit in full page for more options like assets, serial nos, batches etc.","Bearbeiten Sie in Vollansicht für weitere Optionen wie Vermögenswerte, Seriennummern, Chargen usw.",
Can't render this file because it is too large.