fix: Calculate depreciation_amount accurately (#27585)
This commit is contained in:
		
						commit
						ec0a72423a
					
				| @ -58,7 +58,8 @@ class GLEntry(Document): | ||||
| 
 | ||||
| 			# Update outstanding amt on against voucher | ||||
| 			if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] | ||||
| 				and self.against_voucher and self.flags.update_outstanding == 'Yes'): | ||||
| 				and self.against_voucher and self.flags.update_outstanding == 'Yes' | ||||
| 				and not frappe.flags.is_reverse_depr_entry): | ||||
| 					update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, | ||||
| 						self.against_voucher) | ||||
| 
 | ||||
|  | ||||
| @ -58,7 +58,10 @@ class JournalEntry(AccountsController): | ||||
| 		if not frappe.flags.in_import: | ||||
| 			self.validate_total_debit_and_credit() | ||||
| 
 | ||||
| 		self.validate_against_jv() | ||||
| 		if not frappe.flags.is_reverse_depr_entry: | ||||
| 			self.validate_against_jv() | ||||
| 			self.validate_stock_accounts() | ||||
| 
 | ||||
| 		self.validate_reference_doc() | ||||
| 		if self.docstatus == 0: | ||||
| 			self.set_against_account() | ||||
| @ -69,7 +72,6 @@ class JournalEntry(AccountsController): | ||||
| 		self.validate_empty_accounts_table() | ||||
| 		self.set_account_and_party_balance() | ||||
| 		self.validate_inter_company_accounts() | ||||
| 		self.validate_stock_accounts() | ||||
| 
 | ||||
| 		if self.docstatus == 0: | ||||
| 			self.apply_tax_withholding() | ||||
|  | ||||
| @ -37,7 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import ( | ||||
| 	get_disposal_account_and_cost_center, | ||||
| 	get_gl_entries_on_asset_disposal, | ||||
| 	get_gl_entries_on_asset_regain, | ||||
| 	post_depreciation_entries, | ||||
| 	make_depreciation_entry, | ||||
| ) | ||||
| from erpnext.controllers.selling_controller import SellingController | ||||
| from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data | ||||
| @ -934,6 +934,7 @@ class SalesInvoice(SellingController): | ||||
| 						asset.db_set("disposal_date", None) | ||||
| 
 | ||||
| 						if asset.calculate_depreciation: | ||||
| 							self.reverse_depreciation_entry_made_after_sale(asset) | ||||
| 							self.reset_depreciation_schedule(asset) | ||||
| 
 | ||||
| 					else: | ||||
| @ -997,22 +998,20 @@ class SalesInvoice(SellingController): | ||||
| 
 | ||||
| 	def depreciate_asset(self, asset): | ||||
| 		asset.flags.ignore_validate_update_after_submit = True | ||||
| 		asset.prepare_depreciation_data(self.posting_date) | ||||
| 		asset.prepare_depreciation_data(date_of_sale=self.posting_date) | ||||
| 		asset.save() | ||||
| 
 | ||||
| 		post_depreciation_entries(self.posting_date) | ||||
| 		make_depreciation_entry(asset.name, self.posting_date) | ||||
| 
 | ||||
| 	def reset_depreciation_schedule(self, asset): | ||||
| 		asset.flags.ignore_validate_update_after_submit = True | ||||
| 
 | ||||
| 		# recreate original depreciation schedule of the asset | ||||
| 		asset.prepare_depreciation_data() | ||||
| 		asset.prepare_depreciation_data(date_of_return=self.posting_date) | ||||
| 
 | ||||
| 		self.modify_depreciation_schedule_for_asset_repairs(asset) | ||||
| 		asset.save() | ||||
| 
 | ||||
| 		self.delete_depreciation_entry_made_after_sale(asset) | ||||
| 
 | ||||
| 	def modify_depreciation_schedule_for_asset_repairs(self, asset): | ||||
| 		asset_repairs = frappe.get_all( | ||||
| 			'Asset Repair', | ||||
| @ -1026,7 +1025,7 @@ class SalesInvoice(SellingController): | ||||
| 				asset_repair.modify_depreciation_schedule() | ||||
| 				asset.prepare_depreciation_data() | ||||
| 
 | ||||
| 	def delete_depreciation_entry_made_after_sale(self, asset): | ||||
| 	def reverse_depreciation_entry_made_after_sale(self, asset): | ||||
| 		from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry | ||||
| 
 | ||||
| 		posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() | ||||
| @ -1041,11 +1040,19 @@ class SalesInvoice(SellingController): | ||||
| 				row += 1 | ||||
| 
 | ||||
| 			if schedule.schedule_date == posting_date_of_original_invoice: | ||||
| 				if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): | ||||
| 				if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \ | ||||
| 					or self.sale_happens_in_the_future(posting_date_of_original_invoice): | ||||
| 
 | ||||
| 					reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) | ||||
| 					reverse_journal_entry.posting_date = nowdate() | ||||
| 					frappe.flags.is_reverse_depr_entry = True | ||||
| 					reverse_journal_entry.submit() | ||||
| 
 | ||||
| 					frappe.flags.is_reverse_depr_entry = False | ||||
| 					asset.flags.ignore_validate_update_after_submit = True | ||||
| 					schedule.journal_entry = None | ||||
| 					asset.save() | ||||
| 
 | ||||
| 	def get_posting_date_of_sales_invoice(self): | ||||
| 		return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') | ||||
| 
 | ||||
| @ -1060,6 +1067,12 @@ class SalesInvoice(SellingController): | ||||
| 					return True | ||||
| 		return False | ||||
| 
 | ||||
| 	def sale_happens_in_the_future(self, posting_date_of_original_invoice): | ||||
| 		if posting_date_of_original_invoice > getdate(): | ||||
| 			return True | ||||
| 
 | ||||
| 		return False | ||||
| 
 | ||||
| 	@property | ||||
| 	def enable_discount_accounting(self): | ||||
| 		if not hasattr(self, "_enable_discount_accounting"): | ||||
|  | ||||
| @ -2237,9 +2237,9 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 		check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) | ||||
| 		enable_discount_accounting(enable=0) | ||||
| 
 | ||||
| 	def test_asset_depreciation_on_sale(self): | ||||
| 	def test_asset_depreciation_on_sale_with_pro_rata(self): | ||||
| 		""" | ||||
| 			Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30. | ||||
| 			Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. | ||||
| 		""" | ||||
| 
 | ||||
| 		create_asset_data() | ||||
| @ -2252,7 +2252,7 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 		expected_values = [ | ||||
| 			["2020-06-30", 1311.48, 1311.48], | ||||
| 			["2021-06-30", 20000.0, 21311.48], | ||||
| 			["2021-09-30", 3966.76, 25278.24] | ||||
| 			["2021-09-30", 5041.1, 26352.58] | ||||
| 		] | ||||
| 
 | ||||
| 		for i, schedule in enumerate(asset.schedules): | ||||
| @ -2261,6 +2261,59 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 			self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) | ||||
| 			self.assertTrue(schedule.journal_entry) | ||||
| 
 | ||||
| 	def test_asset_depreciation_on_sale_without_pro_rata(self): | ||||
| 		""" | ||||
| 			Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. | ||||
| 		""" | ||||
| 
 | ||||
| 		create_asset_data() | ||||
| 		asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, | ||||
| 			available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3, | ||||
| 			expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1) | ||||
| 
 | ||||
| 		post_depreciation_entries(getdate("2021-09-30")) | ||||
| 
 | ||||
| 		create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31")) | ||||
| 		asset.load_from_db() | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			["2020-12-31", 30000, 30000], | ||||
| 			["2021-12-31", 30000, 60000] | ||||
| 		] | ||||
| 
 | ||||
| 		for i, schedule in enumerate(asset.schedules): | ||||
| 			self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) | ||||
| 			self.assertEqual(expected_values[i][1], schedule.depreciation_amount) | ||||
| 			self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) | ||||
| 			self.assertTrue(schedule.journal_entry) | ||||
| 
 | ||||
| 	def test_depreciation_on_return_of_sold_asset(self): | ||||
| 		from erpnext.controllers.sales_and_purchase_return import make_return_doc | ||||
| 
 | ||||
| 		create_asset_data() | ||||
| 		asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) | ||||
| 		post_depreciation_entries(getdate("2021-09-30")) | ||||
| 
 | ||||
| 		si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) | ||||
| 		return_si = make_return_doc("Sales Invoice", si.name) | ||||
| 		return_si.submit() | ||||
| 		asset.load_from_db() | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			["2020-06-30", 1311.48, 1311.48, True], | ||||
| 			["2021-06-30", 20000.0, 21311.48, True], | ||||
| 			["2022-06-30", 20000.0, 41311.48, False], | ||||
| 			["2023-06-30", 20000.0, 61311.48, False], | ||||
| 			["2024-06-30",  20000.0, 81311.48,  False], | ||||
| 			["2025-06-06",  18688.52,  100000.0, False] | ||||
| 		] | ||||
| 
 | ||||
| 		for i, schedule in enumerate(asset.schedules): | ||||
| 			self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) | ||||
| 			self.assertEqual(expected_values[i][1], schedule.depreciation_amount) | ||||
| 			self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) | ||||
| 			self.assertEqual(schedule.journal_entry, schedule.journal_entry) | ||||
| 
 | ||||
| 	def test_sales_invoice_against_supplier(self): | ||||
| 		from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( | ||||
| 			make_customer, | ||||
|  | ||||
| @ -75,12 +75,12 @@ class Asset(AccountsController): | ||||
| 		if self.is_existing_asset and self.purchase_invoice: | ||||
| 			frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) | ||||
| 
 | ||||
| 	def prepare_depreciation_data(self, date_of_sale=None): | ||||
| 	def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): | ||||
| 		if self.calculate_depreciation: | ||||
| 			self.value_after_depreciation = 0 | ||||
| 			self.set_depreciation_rate() | ||||
| 			self.make_depreciation_schedule(date_of_sale) | ||||
| 			self.set_accumulated_depreciation(date_of_sale) | ||||
| 			self.set_accumulated_depreciation(date_of_sale, date_of_return) | ||||
| 		else: | ||||
| 			self.finance_books = [] | ||||
| 			self.value_after_depreciation = (flt(self.gross_purchase_amount) - | ||||
| @ -182,7 +182,7 @@ class Asset(AccountsController): | ||||
| 				d.precision("rate_of_depreciation")) | ||||
| 
 | ||||
| 	def make_depreciation_schedule(self, date_of_sale): | ||||
| 		if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: | ||||
| 		if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'): | ||||
| 			self.schedules = [] | ||||
| 
 | ||||
| 		if not self.available_for_use_date: | ||||
| @ -232,13 +232,15 @@ class Asset(AccountsController): | ||||
| 					depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, | ||||
| 						from_date, date_of_sale) | ||||
| 
 | ||||
| 					self.append("schedules", { | ||||
| 						"schedule_date": date_of_sale, | ||||
| 						"depreciation_amount": depreciation_amount, | ||||
| 						"depreciation_method": d.depreciation_method, | ||||
| 						"finance_book": d.finance_book, | ||||
| 						"finance_book_id": d.idx | ||||
| 					}) | ||||
| 					if depreciation_amount > 0: | ||||
| 						self.append("schedules", { | ||||
| 							"schedule_date": date_of_sale, | ||||
| 							"depreciation_amount": depreciation_amount, | ||||
| 							"depreciation_method": d.depreciation_method, | ||||
| 							"finance_book": d.finance_book, | ||||
| 							"finance_book_id": d.idx | ||||
| 						}) | ||||
| 
 | ||||
| 					break | ||||
| 
 | ||||
| 				# For first row | ||||
| @ -257,11 +259,15 @@ class Asset(AccountsController): | ||||
| 						self.to_date = add_months(self.available_for_use_date, | ||||
| 							n * cint(d.frequency_of_depreciation)) | ||||
| 
 | ||||
| 					depreciation_amount_without_pro_rata = depreciation_amount | ||||
| 
 | ||||
| 					depreciation_amount, days, months = self.get_pro_rata_amt(d, | ||||
| 						depreciation_amount, schedule_date, self.to_date) | ||||
| 
 | ||||
| 					monthly_schedule_date = add_months(schedule_date, 1) | ||||
| 					depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, | ||||
| 						depreciation_amount, d.finance_book) | ||||
| 
 | ||||
| 					monthly_schedule_date = add_months(schedule_date, 1) | ||||
| 					schedule_date = add_days(schedule_date, days) | ||||
| 					last_schedule_date = schedule_date | ||||
| 
 | ||||
| @ -397,7 +403,28 @@ class Asset(AccountsController): | ||||
| 			frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date") | ||||
| 				.format(row.idx)) | ||||
| 
 | ||||
| 	def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False): | ||||
| 	# to ensure that final accumulated depreciation amount is accurate | ||||
| 	def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): | ||||
| 		depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) | ||||
| 
 | ||||
| 		if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: | ||||
| 			depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row | ||||
| 
 | ||||
| 		return depreciation_amount_for_last_row | ||||
| 
 | ||||
| 	def get_depreciation_amount_for_first_row(self, finance_book): | ||||
| 		if self.has_only_one_finance_book(): | ||||
| 			return self.schedules[0].depreciation_amount | ||||
| 		else: | ||||
| 			for schedule in self.schedules: | ||||
| 				if schedule.finance_book == finance_book: | ||||
| 					return schedule.depreciation_amount | ||||
| 
 | ||||
| 	def has_only_one_finance_book(self): | ||||
| 		if len(self.finance_books) == 1: | ||||
| 			return True | ||||
| 
 | ||||
| 	def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False): | ||||
| 		straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] | ||||
| 		finance_books = [] | ||||
| 
 | ||||
| @ -414,7 +441,7 @@ class Asset(AccountsController): | ||||
| 			value_after_depreciation -= flt(depreciation_amount) | ||||
| 
 | ||||
| 			# for the last row, if depreciation method = Straight Line | ||||
| 			if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale: | ||||
| 			if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return: | ||||
| 				book = self.get('finance_books')[cint(d.finance_book_id) - 1] | ||||
| 				depreciation_amount += flt(value_after_depreciation - | ||||
| 					flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) | ||||
| @ -833,7 +860,7 @@ def get_depreciation_amount(asset, depreciable_value, row): | ||||
| 	if row.depreciation_method in ("Straight Line", "Manual"): | ||||
| 		# if the Depreciation Schedule is being prepared for the first time | ||||
| 		if not asset.flags.increase_in_asset_life: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 			depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 
 | ||||
| 		# if the Depreciation Schedule is being modified after Asset Repair | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -855,7 +855,7 @@ def get_depreciation_amount(asset, depreciable_value, row): | ||||
| 	if row.depreciation_method in ("Straight Line", "Manual"): | ||||
| 		# if the Depreciation Schedule is being prepared for the first time | ||||
| 		if not asset.flags.increase_in_asset_life: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 			depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 
 | ||||
| 		# if the Depreciation Schedule is being modified after Asset Repair | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user