Merge pull request #38007 from frappe/mergify/bp/version-15-hotfix/pr-37828
fix: payments irrespective of party types (backport #37828)
This commit is contained in:
		
						commit
						1b103faf05
					
				| @ -33,6 +33,7 @@ from erpnext.accounts.utils import ( | |||||||
| 	get_account_currency, | 	get_account_currency, | ||||||
| 	get_balance_on, | 	get_balance_on, | ||||||
| 	get_outstanding_invoices, | 	get_outstanding_invoices, | ||||||
|  | 	get_party_types_from_account_type, | ||||||
| ) | ) | ||||||
| from erpnext.controllers.accounts_controller import ( | from erpnext.controllers.accounts_controller import ( | ||||||
| 	AccountsController, | 	AccountsController, | ||||||
| @ -83,7 +84,6 @@ class PaymentEntry(AccountsController): | |||||||
| 		self.apply_taxes() | 		self.apply_taxes() | ||||||
| 		self.set_amounts_after_tax() | 		self.set_amounts_after_tax() | ||||||
| 		self.clear_unallocated_reference_document_rows() | 		self.clear_unallocated_reference_document_rows() | ||||||
| 		self.validate_payment_against_negative_invoice() |  | ||||||
| 		self.validate_transaction_reference() | 		self.validate_transaction_reference() | ||||||
| 		self.set_title() | 		self.set_title() | ||||||
| 		self.set_remarks() | 		self.set_remarks() | ||||||
| @ -952,35 +952,6 @@ class PaymentEntry(AccountsController): | |||||||
| 			self.name, | 			self.name, | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 	def validate_payment_against_negative_invoice(self): |  | ||||||
| 		if (self.payment_type != "Pay" or self.party_type != "Customer") and ( |  | ||||||
| 			self.payment_type != "Receive" or self.party_type != "Supplier" |  | ||||||
| 		): |  | ||||||
| 			return |  | ||||||
| 
 |  | ||||||
| 		total_negative_outstanding = sum( |  | ||||||
| 			abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 |  | ||||||
| 		) |  | ||||||
| 
 |  | ||||||
| 		paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount |  | ||||||
| 		additional_charges = sum(flt(d.amount) for d in self.deductions) |  | ||||||
| 
 |  | ||||||
| 		if not total_negative_outstanding: |  | ||||||
| 			if self.party_type == "Customer": |  | ||||||
| 				msg = _("Cannot pay to Customer without any negative outstanding invoice") |  | ||||||
| 			else: |  | ||||||
| 				msg = _("Cannot receive from Supplier without any negative outstanding invoice") |  | ||||||
| 
 |  | ||||||
| 			frappe.throw(msg, InvalidPaymentEntry) |  | ||||||
| 
 |  | ||||||
| 		elif paid_amount - additional_charges > total_negative_outstanding: |  | ||||||
| 			frappe.throw( |  | ||||||
| 				_("Paid Amount cannot be greater than total negative outstanding amount {0}").format( |  | ||||||
| 					fmt_money(total_negative_outstanding) |  | ||||||
| 				), |  | ||||||
| 				InvalidPaymentEntry, |  | ||||||
| 			) |  | ||||||
| 
 |  | ||||||
| 	def set_title(self): | 	def set_title(self): | ||||||
| 		if frappe.flags.in_import and self.title: | 		if frappe.flags.in_import and self.title: | ||||||
| 			# do not set title dynamically if title exists during data import. | 			# do not set title dynamically if title exists during data import. | ||||||
| @ -1083,9 +1054,7 @@ class PaymentEntry(AccountsController): | |||||||
| 				item=self, | 				item=self, | ||||||
| 			) | 			) | ||||||
| 
 | 
 | ||||||
| 			dr_or_cr = ( | 			dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" | ||||||
| 				"credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit" |  | ||||||
| 			) |  | ||||||
| 
 | 
 | ||||||
| 			for d in self.get("references"): | 			for d in self.get("references"): | ||||||
| 				cost_center = self.cost_center | 				cost_center = self.cost_center | ||||||
| @ -1103,10 +1072,27 @@ class PaymentEntry(AccountsController): | |||||||
| 					against_voucher_type = d.reference_doctype | 					against_voucher_type = d.reference_doctype | ||||||
| 					against_voucher = d.reference_name | 					against_voucher = d.reference_name | ||||||
| 
 | 
 | ||||||
|  | 				reverse_dr_or_cr, standalone_note = 0, 0 | ||||||
|  | 				if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]: | ||||||
|  | 					is_return, return_against = frappe.db.get_value( | ||||||
|  | 						d.reference_doctype, d.reference_name, ["is_return", "return_against"] | ||||||
|  | 					) | ||||||
|  | 					payable_party_types = get_party_types_from_account_type("Payable") | ||||||
|  | 					receivable_party_types = get_party_types_from_account_type("Receivable") | ||||||
|  | 					if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"): | ||||||
|  | 						reverse_dr_or_cr = 1 | ||||||
|  | 					elif ( | ||||||
|  | 						is_return and self.party_type in payable_party_types and (self.payment_type == "Receive") | ||||||
|  | 					): | ||||||
|  | 						reverse_dr_or_cr = 1 | ||||||
|  | 
 | ||||||
|  | 					if is_return and not return_against and not reverse_dr_or_cr: | ||||||
|  | 						dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" | ||||||
|  | 
 | ||||||
| 				gle.update( | 				gle.update( | ||||||
| 					{ | 					{ | ||||||
| 						dr_or_cr: allocated_amount_in_company_currency, | 						dr_or_cr: abs(allocated_amount_in_company_currency), | ||||||
| 						dr_or_cr + "_in_account_currency": d.allocated_amount, | 						dr_or_cr + "_in_account_currency": abs(d.allocated_amount), | ||||||
| 						"against_voucher_type": against_voucher_type, | 						"against_voucher_type": against_voucher_type, | ||||||
| 						"against_voucher": against_voucher, | 						"against_voucher": against_voucher, | ||||||
| 						"cost_center": cost_center, | 						"cost_center": cost_center, | ||||||
|  | |||||||
| @ -683,17 +683,6 @@ class TestPaymentEntry(FrappeTestCase): | |||||||
| 		self.validate_gl_entries(pe.name, expected_gle) | 		self.validate_gl_entries(pe.name, expected_gle) | ||||||
| 
 | 
 | ||||||
| 	def test_payment_against_negative_sales_invoice(self): | 	def test_payment_against_negative_sales_invoice(self): | ||||||
| 		pe1 = frappe.new_doc("Payment Entry") |  | ||||||
| 		pe1.payment_type = "Pay" |  | ||||||
| 		pe1.company = "_Test Company" |  | ||||||
| 		pe1.party_type = "Customer" |  | ||||||
| 		pe1.party = "_Test Customer" |  | ||||||
| 		pe1.paid_from = "_Test Cash - _TC" |  | ||||||
| 		pe1.paid_amount = 100 |  | ||||||
| 		pe1.received_amount = 100 |  | ||||||
| 
 |  | ||||||
| 		self.assertRaises(InvalidPaymentEntry, pe1.validate) |  | ||||||
| 
 |  | ||||||
| 		si1 = create_sales_invoice() | 		si1 = create_sales_invoice() | ||||||
| 
 | 
 | ||||||
| 		# create full payment entry against si1 | 		# create full payment entry against si1 | ||||||
| @ -751,8 +740,6 @@ class TestPaymentEntry(FrappeTestCase): | |||||||
| 
 | 
 | ||||||
| 		# pay more than outstanding against si1 | 		# pay more than outstanding against si1 | ||||||
| 		pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") | 		pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") | ||||||
| 		pe3.paid_amount = pe3.received_amount = 300 |  | ||||||
| 		self.assertRaises(InvalidPaymentEntry, pe3.validate) |  | ||||||
| 
 | 
 | ||||||
| 		# pay negative outstanding against si1 | 		# pay negative outstanding against si1 | ||||||
| 		pe3.paid_to = "Debtors - _TC" | 		pe3.paid_to = "Debtors - _TC" | ||||||
| @ -1262,6 +1249,39 @@ class TestPaymentEntry(FrappeTestCase): | |||||||
| 		so.reload() | 		so.reload() | ||||||
| 		self.assertEqual(so.advance_paid, so.rounded_total) | 		self.assertEqual(so.advance_paid, so.rounded_total) | ||||||
| 
 | 
 | ||||||
|  | 	def test_receive_payment_from_payable_party_type(self): | ||||||
|  | 		pe = create_payment_entry( | ||||||
|  | 			party_type="Supplier", | ||||||
|  | 			party="_Test Supplier", | ||||||
|  | 			payment_type="Receive", | ||||||
|  | 			paid_from="Creditors - _TC", | ||||||
|  | 			paid_to="_Test Cash - _TC", | ||||||
|  | 			save=True, | ||||||
|  | 			submit=True, | ||||||
|  | 		) | ||||||
|  | 		self.voucher_no = pe.name | ||||||
|  | 		self.expected_gle = [ | ||||||
|  | 			{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, | ||||||
|  | 			{"account": "Creditors - _TC", "debit": 0.0, "credit": 1000.0}, | ||||||
|  | 		] | ||||||
|  | 		self.check_gl_entries() | ||||||
|  | 
 | ||||||
|  | 	def check_gl_entries(self): | ||||||
|  | 		gle = frappe.qb.DocType("GL Entry") | ||||||
|  | 		gl_entries = ( | ||||||
|  | 			frappe.qb.from_(gle) | ||||||
|  | 			.select( | ||||||
|  | 				gle.account, | ||||||
|  | 				gle.debit, | ||||||
|  | 				gle.credit, | ||||||
|  | 			) | ||||||
|  | 			.where((gle.voucher_no == self.voucher_no) & (gle.is_cancelled == 0)) | ||||||
|  | 			.orderby(gle.account) | ||||||
|  | 		).run(as_dict=True) | ||||||
|  | 		for row in range(len(self.expected_gle)): | ||||||
|  | 			for field in ["account", "debit", "credit"]: | ||||||
|  | 				self.assertEqual(self.expected_gle[row][field], gl_entries[row][field]) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def create_payment_entry(**args): | def create_payment_entry(**args): | ||||||
| 	payment_entry = frappe.new_doc("Payment Entry") | 	payment_entry = frappe.new_doc("Payment Entry") | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( | |||||||
| 	get_accounting_dimensions, | 	get_accounting_dimensions, | ||||||
| 	get_dimension_with_children, | 	get_dimension_with_children, | ||||||
| ) | ) | ||||||
| from erpnext.accounts.utils import get_currency_precision | from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type | ||||||
| 
 | 
 | ||||||
| #  This report gives a summary of all Outstanding Invoices considering the following | #  This report gives a summary of all Outstanding Invoices considering the following | ||||||
| 
 | 
 | ||||||
| @ -72,9 +72,7 @@ class ReceivablePayableReport(object): | |||||||
| 		self.currency_precision = get_currency_precision() or 2 | 		self.currency_precision = get_currency_precision() or 2 | ||||||
| 		self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" | 		self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" | ||||||
| 		self.account_type = self.filters.account_type | 		self.account_type = self.filters.account_type | ||||||
| 		self.party_type = frappe.db.get_all( | 		self.party_type = get_party_types_from_account_type(self.account_type) | ||||||
| 			"Party Type", {"account_type": self.account_type}, pluck="name" |  | ||||||
| 		) |  | ||||||
| 		self.party_details = {} | 		self.party_details = {} | ||||||
| 		self.invoices = set() | 		self.invoices = set() | ||||||
| 		self.skip_total_row = 0 | 		self.skip_total_row = 0 | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from frappe.utils import cint, flt | |||||||
| 
 | 
 | ||||||
| from erpnext.accounts.party import get_partywise_advanced_payment_amount | from erpnext.accounts.party import get_partywise_advanced_payment_amount | ||||||
| from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport | from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport | ||||||
|  | from erpnext.accounts.utils import get_party_types_from_account_type | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def execute(filters=None): | def execute(filters=None): | ||||||
| @ -22,9 +23,7 @@ def execute(filters=None): | |||||||
| class AccountsReceivableSummary(ReceivablePayableReport): | class AccountsReceivableSummary(ReceivablePayableReport): | ||||||
| 	def run(self, args): | 	def run(self, args): | ||||||
| 		self.account_type = args.get("account_type") | 		self.account_type = args.get("account_type") | ||||||
| 		self.party_type = frappe.db.get_all( | 		self.party_type = get_party_types_from_account_type(self.account_type) | ||||||
| 			"Party Type", {"account_type": self.account_type}, pluck="name" |  | ||||||
| 		) |  | ||||||
| 		self.party_naming_by = frappe.db.get_value( | 		self.party_naming_by = frappe.db.get_value( | ||||||
| 			args.get("naming_by")[0], None, args.get("naming_by")[1] | 			args.get("naming_by")[0], None, args.get("naming_by")[1] | ||||||
| 		) | 		) | ||||||
|  | |||||||
| @ -316,7 +316,7 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts): | |||||||
| 	if not tds_accounts: | 	if not tds_accounts: | ||||||
| 		frappe.throw( | 		frappe.throw( | ||||||
| 			_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), | 			_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), | ||||||
| 			title="Accounts Missing Error", | 			title=_("Accounts Missing Error"), | ||||||
| 		) | 		) | ||||||
| 	gle = frappe.qb.DocType("GL Entry") | 	gle = frappe.qb.DocType("GL Entry") | ||||||
| 	query = ( | 	query = ( | ||||||
|  | |||||||
| @ -2040,3 +2040,7 @@ def create_gain_loss_journal( | |||||||
| 	journal_entry.save() | 	journal_entry.save() | ||||||
| 	journal_entry.submit() | 	journal_entry.submit() | ||||||
| 	return journal_entry.name | 	return journal_entry.name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_party_types_from_account_type(account_type): | ||||||
|  | 	return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name") | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user