Merge branch 'develop' into subcontracting
This commit is contained in:
		
						commit
						07dc5f180d
					
				| @ -94,7 +94,7 @@ class JournalEntry(AccountsController): | ||||
| 
 | ||||
| 		unlink_ref_doc_from_payment_entries(self) | ||||
| 		unlink_ref_doc_from_salary_slip(self.name) | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") | ||||
| 		self.make_gl_entries(1) | ||||
| 		self.update_advance_paid() | ||||
| 		self.update_expense_claim() | ||||
|  | ||||
| @ -95,7 +95,7 @@ class PaymentEntry(AccountsController): | ||||
| 		self.set_status() | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") | ||||
| 		self.make_gl_entries(cancel=1) | ||||
| 		self.update_expense_claim() | ||||
| 		self.update_outstanding_amounts() | ||||
|  | ||||
| @ -0,0 +1,8 @@ | ||||
| // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Payment Ledger Entry', { | ||||
| 	// refresh: function(frm) {
 | ||||
| 
 | ||||
| 	// }
 | ||||
| }); | ||||
| @ -0,0 +1,180 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "format:PLE-{YY}-{MM}-{######}", | ||||
|  "creation": "2022-05-09 19:35:03.334361", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "posting_date", | ||||
|   "company", | ||||
|   "account_type", | ||||
|   "account", | ||||
|   "party_type", | ||||
|   "party", | ||||
|   "due_date", | ||||
|   "cost_center", | ||||
|   "finance_book", | ||||
|   "voucher_type", | ||||
|   "voucher_no", | ||||
|   "against_voucher_type", | ||||
|   "against_voucher_no", | ||||
|   "amount", | ||||
|   "account_currency", | ||||
|   "amount_in_account_currency", | ||||
|   "delinked" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Posting Date" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "account_type", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Account Type", | ||||
|    "options": "Receivable\nPayable" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Account", | ||||
|    "options": "Account" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "party_type", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Party Type", | ||||
|    "options": "DocType" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "party", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "label": "Party", | ||||
|    "options": "party_type" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "voucher_type", | ||||
|    "fieldtype": "Link", | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Voucher Type", | ||||
|    "options": "DocType" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "voucher_no", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Voucher No", | ||||
|    "options": "voucher_type" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "against_voucher_type", | ||||
|    "fieldtype": "Link", | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Against Voucher Type", | ||||
|    "options": "DocType" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "against_voucher_no", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Against Voucher No", | ||||
|    "options": "against_voucher_type" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "options": "Company:company:default_currency" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "account_currency", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Currency", | ||||
|    "options": "Currency" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amount_in_account_currency", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Amount in Account Currency", | ||||
|    "options": "account_currency" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "delinked", | ||||
|    "fieldtype": "Check", | ||||
|    "in_list_view": 1, | ||||
|    "label": "DeLinked" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "cost_center", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cost Center", | ||||
|    "options": "Cost Center" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "due_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Due Date" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "finance_book", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Finance Book", | ||||
|    "options": "Finance Book" | ||||
|   } | ||||
|  ], | ||||
|  "in_create": 1, | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "links": [], | ||||
|  "modified": "2022-05-19 18:04:44.609115", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Ledger Entry", | ||||
|  "naming_rule": "Expression", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts User", | ||||
|    "share": 1 | ||||
|   }, | ||||
|   { | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts Manager", | ||||
|    "share": 1 | ||||
|   }, | ||||
|   { | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Auditor", | ||||
|    "share": 1 | ||||
|   } | ||||
|  ], | ||||
|  "search_fields": "voucher_no, against_voucher_no", | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "states": [] | ||||
| } | ||||
| @ -0,0 +1,22 @@ | ||||
| # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| 
 | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| 
 | ||||
| class PaymentLedgerEntry(Document): | ||||
| 	def validate_account(self): | ||||
| 		valid_account = frappe.db.get_list( | ||||
| 			"Account", | ||||
| 			"name", | ||||
| 			filters={"name": self.account, "account_type": self.account_type, "company": self.company}, | ||||
| 			ignore_permissions=True, | ||||
| 		) | ||||
| 		if not valid_account: | ||||
| 			frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type)) | ||||
| 
 | ||||
| 	def validate(self): | ||||
| 		self.validate_account() | ||||
| @ -0,0 +1,408 @@ | ||||
| # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| 
 | ||||
| import frappe | ||||
| from frappe import qb | ||||
| from frappe.tests.utils import FrappeTestCase | ||||
| from frappe.utils import nowdate | ||||
| 
 | ||||
| from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry | ||||
| from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry | ||||
| from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice | ||||
| from erpnext.stock.doctype.item.test_item import create_item | ||||
| 
 | ||||
| 
 | ||||
| class TestPaymentLedgerEntry(FrappeTestCase): | ||||
| 	def setUp(self): | ||||
| 		self.ple = qb.DocType("Payment Ledger Entry") | ||||
| 		self.create_company() | ||||
| 		self.create_item() | ||||
| 		self.create_customer() | ||||
| 		self.clear_old_entries() | ||||
| 
 | ||||
| 	def tearDown(self): | ||||
| 		frappe.db.rollback() | ||||
| 
 | ||||
| 	def create_company(self): | ||||
| 		company_name = "_Test Payment Ledger" | ||||
| 		company = None | ||||
| 		if frappe.db.exists("Company", company_name): | ||||
| 			company = frappe.get_doc("Company", company_name) | ||||
| 		else: | ||||
| 			company = frappe.get_doc( | ||||
| 				{ | ||||
| 					"doctype": "Company", | ||||
| 					"company_name": company_name, | ||||
| 					"country": "India", | ||||
| 					"default_currency": "INR", | ||||
| 					"create_chart_of_accounts_based_on": "Standard Template", | ||||
| 					"chart_of_accounts": "Standard", | ||||
| 				} | ||||
| 			) | ||||
| 			company = company.save() | ||||
| 
 | ||||
| 		self.company = company.name | ||||
| 		self.cost_center = company.cost_center | ||||
| 		self.warehouse = "All Warehouses - _PL" | ||||
| 		self.income_account = "Sales - _PL" | ||||
| 		self.expense_account = "Cost of Goods Sold - _PL" | ||||
| 		self.debit_to = "Debtors - _PL" | ||||
| 		self.creditors = "Creditors - _PL" | ||||
| 
 | ||||
| 		# create bank account | ||||
| 		if frappe.db.exists("Account", "HDFC - _PL"): | ||||
| 			self.bank = "HDFC - _PL" | ||||
| 		else: | ||||
| 			bank_acc = frappe.get_doc( | ||||
| 				{ | ||||
| 					"doctype": "Account", | ||||
| 					"account_name": "HDFC", | ||||
| 					"parent_account": "Bank Accounts - _PL", | ||||
| 					"company": self.company, | ||||
| 				} | ||||
| 			) | ||||
| 			bank_acc.save() | ||||
| 			self.bank = bank_acc.name | ||||
| 
 | ||||
| 	def create_item(self): | ||||
| 		item_name = "_Test PL Item" | ||||
| 		item = create_item( | ||||
| 			item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse | ||||
| 		) | ||||
| 		self.item = item if isinstance(item, str) else item.item_code | ||||
| 
 | ||||
| 	def create_customer(self): | ||||
| 		name = "_Test PL Customer" | ||||
| 		if frappe.db.exists("Customer", name): | ||||
| 			self.customer = name | ||||
| 		else: | ||||
| 			customer = frappe.new_doc("Customer") | ||||
| 			customer.customer_name = name | ||||
| 			customer.type = "Individual" | ||||
| 			customer.save() | ||||
| 			self.customer = customer.name | ||||
| 
 | ||||
| 	def create_sales_invoice( | ||||
| 		self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False | ||||
| 	): | ||||
| 		""" | ||||
| 		Helper function to populate default values in sales invoice | ||||
| 		""" | ||||
| 		sinv = create_sales_invoice( | ||||
| 			qty=qty, | ||||
| 			rate=rate, | ||||
| 			company=self.company, | ||||
| 			customer=self.customer, | ||||
| 			item_code=self.item, | ||||
| 			item_name=self.item, | ||||
| 			cost_center=self.cost_center, | ||||
| 			warehouse=self.warehouse, | ||||
| 			debit_to=self.debit_to, | ||||
| 			parent_cost_center=self.cost_center, | ||||
| 			update_stock=0, | ||||
| 			currency="INR", | ||||
| 			is_pos=0, | ||||
| 			is_return=0, | ||||
| 			return_against=None, | ||||
| 			income_account=self.income_account, | ||||
| 			expense_account=self.expense_account, | ||||
| 			do_not_save=do_not_save, | ||||
| 			do_not_submit=do_not_submit, | ||||
| 		) | ||||
| 		return sinv | ||||
| 
 | ||||
| 	def create_payment_entry(self, amount=100, posting_date=nowdate()): | ||||
| 		""" | ||||
| 		Helper function to populate default values in payment entry | ||||
| 		""" | ||||
| 		payment = create_payment_entry( | ||||
| 			company=self.company, | ||||
| 			payment_type="Receive", | ||||
| 			party_type="Customer", | ||||
| 			party=self.customer, | ||||
| 			paid_from=self.debit_to, | ||||
| 			paid_to=self.bank, | ||||
| 			paid_amount=amount, | ||||
| 		) | ||||
| 		payment.posting_date = posting_date | ||||
| 		return payment | ||||
| 
 | ||||
| 	def clear_old_entries(self): | ||||
| 		doctype_list = [ | ||||
| 			"GL Entry", | ||||
| 			"Payment Ledger Entry", | ||||
| 			"Sales Invoice", | ||||
| 			"Purchase Invoice", | ||||
| 			"Payment Entry", | ||||
| 			"Journal Entry", | ||||
| 		] | ||||
| 		for doctype in doctype_list: | ||||
| 			qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() | ||||
| 
 | ||||
| 	def create_journal_entry( | ||||
| 		self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None | ||||
| 	): | ||||
| 		je = frappe.new_doc("Journal Entry") | ||||
| 		je.posting_date = posting_date or nowdate() | ||||
| 		je.company = self.company | ||||
| 		je.user_remark = "test" | ||||
| 		if not cost_center: | ||||
| 			cost_center = self.cost_center | ||||
| 		je.set( | ||||
| 			"accounts", | ||||
| 			[ | ||||
| 				{ | ||||
| 					"account": acc1, | ||||
| 					"cost_center": cost_center, | ||||
| 					"debit_in_account_currency": amount if amount > 0 else 0, | ||||
| 					"credit_in_account_currency": abs(amount) if amount < 0 else 0, | ||||
| 				}, | ||||
| 				{ | ||||
| 					"account": acc2, | ||||
| 					"cost_center": cost_center, | ||||
| 					"credit_in_account_currency": amount if amount > 0 else 0, | ||||
| 					"debit_in_account_currency": abs(amount) if amount < 0 else 0, | ||||
| 				}, | ||||
| 			], | ||||
| 		) | ||||
| 		return je | ||||
| 
 | ||||
| 	def test_payment_against_invoice(self): | ||||
| 		transaction_date = nowdate() | ||||
| 		amount = 100 | ||||
| 		ple = self.ple | ||||
| 
 | ||||
| 		# full payment using PE | ||||
| 		si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) | ||||
| 		pe1 = get_payment_entry(si1.doctype, si1.name).save().submit() | ||||
| 
 | ||||
| 		pl_entries = ( | ||||
| 			qb.from_(ple) | ||||
| 			.select( | ||||
| 				ple.voucher_type, | ||||
| 				ple.voucher_no, | ||||
| 				ple.against_voucher_type, | ||||
| 				ple.against_voucher_no, | ||||
| 				ple.amount, | ||||
| 				ple.delinked, | ||||
| 			) | ||||
| 			.where((ple.against_voucher_type == si1.doctype) & (ple.against_voucher_no == si1.name)) | ||||
| 			.orderby(ple.creation) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			{ | ||||
| 				"voucher_type": si1.doctype, | ||||
| 				"voucher_no": si1.name, | ||||
| 				"against_voucher_type": si1.doctype, | ||||
| 				"against_voucher_no": si1.name, | ||||
| 				"amount": amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 			{ | ||||
| 				"voucher_type": pe1.doctype, | ||||
| 				"voucher_no": pe1.name, | ||||
| 				"against_voucher_type": si1.doctype, | ||||
| 				"against_voucher_no": si1.name, | ||||
| 				"amount": -amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 		] | ||||
| 		self.assertEqual(pl_entries[0], expected_values[0]) | ||||
| 		self.assertEqual(pl_entries[1], expected_values[1]) | ||||
| 
 | ||||
| 	def test_partial_payment_against_invoice(self): | ||||
| 		ple = self.ple | ||||
| 		transaction_date = nowdate() | ||||
| 		amount = 100 | ||||
| 
 | ||||
| 		# partial payment of invoice using PE | ||||
| 		si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) | ||||
| 		pe2 = get_payment_entry(si2.doctype, si2.name) | ||||
| 		pe2.get("references")[0].allocated_amount = 50 | ||||
| 		pe2.get("references")[0].outstanding_amount = 50 | ||||
| 		pe2 = pe2.save().submit() | ||||
| 
 | ||||
| 		pl_entries = ( | ||||
| 			qb.from_(ple) | ||||
| 			.select( | ||||
| 				ple.voucher_type, | ||||
| 				ple.voucher_no, | ||||
| 				ple.against_voucher_type, | ||||
| 				ple.against_voucher_no, | ||||
| 				ple.amount, | ||||
| 				ple.delinked, | ||||
| 			) | ||||
| 			.where((ple.against_voucher_type == si2.doctype) & (ple.against_voucher_no == si2.name)) | ||||
| 			.orderby(ple.creation) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			{ | ||||
| 				"voucher_type": si2.doctype, | ||||
| 				"voucher_no": si2.name, | ||||
| 				"against_voucher_type": si2.doctype, | ||||
| 				"against_voucher_no": si2.name, | ||||
| 				"amount": amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 			{ | ||||
| 				"voucher_type": pe2.doctype, | ||||
| 				"voucher_no": pe2.name, | ||||
| 				"against_voucher_type": si2.doctype, | ||||
| 				"against_voucher_no": si2.name, | ||||
| 				"amount": -50, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 		] | ||||
| 		self.assertEqual(pl_entries[0], expected_values[0]) | ||||
| 		self.assertEqual(pl_entries[1], expected_values[1]) | ||||
| 
 | ||||
| 	def test_cr_note_against_invoice(self): | ||||
| 		ple = self.ple | ||||
| 		transaction_date = nowdate() | ||||
| 		amount = 100 | ||||
| 
 | ||||
| 		# reconcile against return invoice | ||||
| 		si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) | ||||
| 		cr_note1 = self.create_sales_invoice( | ||||
| 			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True | ||||
| 		) | ||||
| 		cr_note1.is_return = 1 | ||||
| 		cr_note1.return_against = si3.name | ||||
| 		cr_note1 = cr_note1.save().submit() | ||||
| 
 | ||||
| 		pl_entries = ( | ||||
| 			qb.from_(ple) | ||||
| 			.select( | ||||
| 				ple.voucher_type, | ||||
| 				ple.voucher_no, | ||||
| 				ple.against_voucher_type, | ||||
| 				ple.against_voucher_no, | ||||
| 				ple.amount, | ||||
| 				ple.delinked, | ||||
| 			) | ||||
| 			.where((ple.against_voucher_type == si3.doctype) & (ple.against_voucher_no == si3.name)) | ||||
| 			.orderby(ple.creation) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			{ | ||||
| 				"voucher_type": si3.doctype, | ||||
| 				"voucher_no": si3.name, | ||||
| 				"against_voucher_type": si3.doctype, | ||||
| 				"against_voucher_no": si3.name, | ||||
| 				"amount": amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 			{ | ||||
| 				"voucher_type": cr_note1.doctype, | ||||
| 				"voucher_no": cr_note1.name, | ||||
| 				"against_voucher_type": si3.doctype, | ||||
| 				"against_voucher_no": si3.name, | ||||
| 				"amount": -amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 		] | ||||
| 		self.assertEqual(pl_entries[0], expected_values[0]) | ||||
| 		self.assertEqual(pl_entries[1], expected_values[1]) | ||||
| 
 | ||||
| 	def test_je_against_inv_and_note(self): | ||||
| 		ple = self.ple | ||||
| 		transaction_date = nowdate() | ||||
| 		amount = 100 | ||||
| 
 | ||||
| 		# reconcile against return invoice using JE | ||||
| 		si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) | ||||
| 		cr_note2 = self.create_sales_invoice( | ||||
| 			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True | ||||
| 		) | ||||
| 		cr_note2.is_return = 1 | ||||
| 		cr_note2 = cr_note2.save().submit() | ||||
| 		je1 = self.create_journal_entry( | ||||
| 			self.debit_to, self.debit_to, amount, posting_date=transaction_date | ||||
| 		) | ||||
| 		je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer" | ||||
| 		je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer | ||||
| 		je1.get("accounts")[0].reference_type = cr_note2.doctype | ||||
| 		je1.get("accounts")[0].reference_name = cr_note2.name | ||||
| 		je1.get("accounts")[1].reference_type = si4.doctype | ||||
| 		je1.get("accounts")[1].reference_name = si4.name | ||||
| 		je1 = je1.save().submit() | ||||
| 
 | ||||
| 		pl_entries_for_invoice = ( | ||||
| 			qb.from_(ple) | ||||
| 			.select( | ||||
| 				ple.voucher_type, | ||||
| 				ple.voucher_no, | ||||
| 				ple.against_voucher_type, | ||||
| 				ple.against_voucher_no, | ||||
| 				ple.amount, | ||||
| 				ple.delinked, | ||||
| 			) | ||||
| 			.where((ple.against_voucher_type == si4.doctype) & (ple.against_voucher_no == si4.name)) | ||||
| 			.orderby(ple.creation) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			{ | ||||
| 				"voucher_type": si4.doctype, | ||||
| 				"voucher_no": si4.name, | ||||
| 				"against_voucher_type": si4.doctype, | ||||
| 				"against_voucher_no": si4.name, | ||||
| 				"amount": amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 			{ | ||||
| 				"voucher_type": je1.doctype, | ||||
| 				"voucher_no": je1.name, | ||||
| 				"against_voucher_type": si4.doctype, | ||||
| 				"against_voucher_no": si4.name, | ||||
| 				"amount": -amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 		] | ||||
| 		self.assertEqual(pl_entries_for_invoice[0], expected_values[0]) | ||||
| 		self.assertEqual(pl_entries_for_invoice[1], expected_values[1]) | ||||
| 
 | ||||
| 		pl_entries_for_crnote = ( | ||||
| 			qb.from_(ple) | ||||
| 			.select( | ||||
| 				ple.voucher_type, | ||||
| 				ple.voucher_no, | ||||
| 				ple.against_voucher_type, | ||||
| 				ple.against_voucher_no, | ||||
| 				ple.amount, | ||||
| 				ple.delinked, | ||||
| 			) | ||||
| 			.where( | ||||
| 				(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name) | ||||
| 			) | ||||
| 			.orderby(ple.creation) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 
 | ||||
| 		expected_values = [ | ||||
| 			{ | ||||
| 				"voucher_type": cr_note2.doctype, | ||||
| 				"voucher_no": cr_note2.name, | ||||
| 				"against_voucher_type": cr_note2.doctype, | ||||
| 				"against_voucher_no": cr_note2.name, | ||||
| 				"amount": -amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 			{ | ||||
| 				"voucher_type": je1.doctype, | ||||
| 				"voucher_no": je1.name, | ||||
| 				"against_voucher_type": cr_note2.doctype, | ||||
| 				"against_voucher_no": cr_note2.name, | ||||
| 				"amount": amount, | ||||
| 				"delinked": 0, | ||||
| 			}, | ||||
| 		] | ||||
| 		self.assertEqual(pl_entries_for_crnote[0], expected_values[0]) | ||||
| 		self.assertEqual(pl_entries_for_crnote[1], expected_values[1]) | ||||
| @ -96,6 +96,7 @@ class POSInvoice(SalesInvoice): | ||||
| 			) | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.ignore_linked_doctypes = "Payment Ledger Entry" | ||||
| 		# run on cancel method of selling controller | ||||
| 		super(SalesInvoice, self).on_cancel() | ||||
| 		if not self.is_return and self.loyalty_program: | ||||
|  | ||||
| @ -1416,7 +1416,12 @@ class PurchaseInvoice(BuyingController): | ||||
| 		frappe.db.set(self, "status", "Cancelled") | ||||
| 
 | ||||
| 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") | ||||
| 		self.ignore_linked_doctypes = ( | ||||
| 			"GL Entry", | ||||
| 			"Stock Ledger Entry", | ||||
| 			"Repost Item Valuation", | ||||
| 			"Payment Ledger Entry", | ||||
| 		) | ||||
| 		self.update_advance_tax_references(cancel=1) | ||||
| 
 | ||||
| 	def update_project(self): | ||||
|  | ||||
| @ -396,7 +396,12 @@ class SalesInvoice(SellingController): | ||||
| 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) | ||||
| 
 | ||||
| 		self.unlink_sales_invoice_from_timesheets() | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") | ||||
| 		self.ignore_linked_doctypes = ( | ||||
| 			"GL Entry", | ||||
| 			"Stock Ledger Entry", | ||||
| 			"Repost Item Valuation", | ||||
| 			"Payment Ledger Entry", | ||||
| 		) | ||||
| 
 | ||||
| 	def update_status_updater_args(self): | ||||
| 		if cint(self.update_stock): | ||||
|  | ||||
| @ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( | ||||
| 	get_accounting_dimensions, | ||||
| ) | ||||
| from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget | ||||
| from erpnext.accounts.utils import create_payment_ledger_entry | ||||
| 
 | ||||
| 
 | ||||
| class ClosedAccountingPeriod(frappe.ValidationError): | ||||
| @ -34,6 +35,7 @@ def make_gl_entries( | ||||
| 			validate_disabled_accounts(gl_map) | ||||
| 			gl_map = process_gl_map(gl_map, merge_entries) | ||||
| 			if gl_map and len(gl_map) > 1: | ||||
| 				create_payment_ledger_entry(gl_map) | ||||
| 				save_entries(gl_map, adv_adj, update_outstanding, from_repost) | ||||
| 			# Post GL Map proccess there may no be any GL Entries | ||||
| 			elif gl_map: | ||||
| @ -479,6 +481,7 @@ def make_reverse_gl_entries( | ||||
| 		).run(as_dict=1) | ||||
| 
 | ||||
| 	if gl_entries: | ||||
| 		create_payment_ledger_entry(gl_entries, cancel=1) | ||||
| 		validate_accounting_period(gl_entries) | ||||
| 		check_freezing_date(gl_entries[0]["posting_date"], adv_adj) | ||||
| 		set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) | ||||
|  | ||||
| @ -7,7 +7,7 @@ from typing import List, Tuple | ||||
| 
 | ||||
| import frappe | ||||
| import frappe.defaults | ||||
| from frappe import _, throw | ||||
| from frappe import _, qb, throw | ||||
| from frappe.model.meta import get_field_precision | ||||
| from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate | ||||
| 
 | ||||
| @ -15,6 +15,7 @@ import erpnext | ||||
| 
 | ||||
| # imported to enable erpnext.accounts.utils.get_account_currency | ||||
| from erpnext.accounts.doctype.account.account import get_account_currency  # noqa | ||||
| from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions | ||||
| from erpnext.stock import get_warehouse_account_map | ||||
| from erpnext.stock.utils import get_stock_value_on | ||||
| 
 | ||||
| @ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report): | ||||
| 	if icons: | ||||
| 		for icon in icons: | ||||
| 			frappe.delete_doc("Desktop Icon", icon) | ||||
| 
 | ||||
| 
 | ||||
| def create_payment_ledger_entry(gl_entries, cancel=0): | ||||
| 	if gl_entries: | ||||
| 		ple = None | ||||
| 
 | ||||
| 		# companies | ||||
| 		account = qb.DocType("Account") | ||||
| 		companies = list(set([x.company for x in gl_entries])) | ||||
| 
 | ||||
| 		# receivable/payable account | ||||
| 		accounts_with_types = ( | ||||
| 			qb.from_(account) | ||||
| 			.select(account.name, account.account_type) | ||||
| 			.where( | ||||
| 				(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies))) | ||||
| 			) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 		receivable_or_payable_accounts = [y.name for y in accounts_with_types] | ||||
| 
 | ||||
| 		def get_account_type(account): | ||||
| 			for entry in accounts_with_types: | ||||
| 				if entry.name == account: | ||||
| 					return entry.account_type | ||||
| 
 | ||||
| 		dr_or_cr = 0 | ||||
| 		account_type = None | ||||
| 		for gle in gl_entries: | ||||
| 			if gle.account in receivable_or_payable_accounts: | ||||
| 				account_type = get_account_type(gle.account) | ||||
| 				if account_type == "Receivable": | ||||
| 					dr_or_cr = gle.debit - gle.credit | ||||
| 					dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency | ||||
| 				elif account_type == "Payable": | ||||
| 					dr_or_cr = gle.credit - gle.debit | ||||
| 					dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency | ||||
| 
 | ||||
| 				if cancel: | ||||
| 					dr_or_cr *= -1 | ||||
| 					dr_or_cr_account_currency *= -1 | ||||
| 
 | ||||
| 				ple = frappe.get_doc( | ||||
| 					{ | ||||
| 						"doctype": "Payment Ledger Entry", | ||||
| 						"posting_date": gle.posting_date, | ||||
| 						"company": gle.company, | ||||
| 						"account_type": account_type, | ||||
| 						"account": gle.account, | ||||
| 						"party_type": gle.party_type, | ||||
| 						"party": gle.party, | ||||
| 						"cost_center": gle.cost_center, | ||||
| 						"finance_book": gle.finance_book, | ||||
| 						"due_date": gle.due_date, | ||||
| 						"voucher_type": gle.voucher_type, | ||||
| 						"voucher_no": gle.voucher_no, | ||||
| 						"against_voucher_type": gle.against_voucher_type | ||||
| 						if gle.against_voucher_type | ||||
| 						else gle.voucher_type, | ||||
| 						"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, | ||||
| 						"currency": gle.currency, | ||||
| 						"amount": dr_or_cr, | ||||
| 						"amount_in_account_currency": dr_or_cr_account_currency, | ||||
| 						"delinked": True if cancel else False, | ||||
| 					} | ||||
| 				) | ||||
| 
 | ||||
| 				dimensions_and_defaults = get_dimensions() | ||||
| 				if dimensions_and_defaults: | ||||
| 					for dimension in dimensions_and_defaults[0]: | ||||
| 						ple.set(dimension.fieldname, gle.get(dimension.fieldname)) | ||||
| 
 | ||||
| 				if cancel: | ||||
| 					delink_original_entry(ple) | ||||
| 				ple.flags.ignore_permissions = 1 | ||||
| 				ple.submit() | ||||
| 
 | ||||
| 
 | ||||
| def delink_original_entry(pl_entry): | ||||
| 	if pl_entry: | ||||
| 		ple = qb.DocType("Payment Ledger Entry") | ||||
| 		query = ( | ||||
| 			qb.update(ple) | ||||
| 			.set(ple.delinked, True) | ||||
| 			.set(ple.modified, now()) | ||||
| 			.set(ple.modified_by, frappe.session.user) | ||||
| 			.where( | ||||
| 				(ple.company == pl_entry.company) | ||||
| 				& (ple.account_type == pl_entry.account_type) | ||||
| 				& (ple.account == pl_entry.account) | ||||
| 				& (ple.party_type == pl_entry.party_type) | ||||
| 				& (ple.party == pl_entry.party) | ||||
| 				& (ple.voucher_type == pl_entry.voucher_type) | ||||
| 				& (ple.voucher_no == pl_entry.voucher_no) | ||||
| 				& (ple.against_voucher_type == pl_entry.against_voucher_type) | ||||
| 				& (ple.against_voucher_no == pl_entry.against_voucher_no) | ||||
| 			) | ||||
| 		) | ||||
| 		query.run() | ||||
|  | ||||
| @ -328,6 +328,7 @@ class PurchaseOrder(BuyingController): | ||||
| 		update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.ignore_linked_doctypes = "Payment Ledger Entry" | ||||
| 		super(PurchaseOrder, self).on_cancel() | ||||
| 
 | ||||
| 		if self.is_against_so(): | ||||
|  | ||||
| @ -487,6 +487,7 @@ communication_doctypes = ["Customer", "Supplier"] | ||||
| 
 | ||||
| accounting_dimension_doctypes = [ | ||||
| 	"GL Entry", | ||||
| 	"Payment Ledger Entry", | ||||
| 	"Sales Invoice", | ||||
| 	"Purchase Invoice", | ||||
| 	"Payment Entry", | ||||
|  | ||||
| @ -105,7 +105,7 @@ class ExpenseClaim(AccountsController): | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.update_task_and_project() | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") | ||||
| 		if self.payable_account: | ||||
| 			self.make_gl_entries(cancel=True) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										38
									
								
								erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import frappe | ||||
| from frappe import qb | ||||
| 
 | ||||
| from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( | ||||
| 	get_dimensions, | ||||
| 	make_dimension_in_accounting_doctypes, | ||||
| ) | ||||
| from erpnext.accounts.utils import create_payment_ledger_entry | ||||
| 
 | ||||
| 
 | ||||
| def create_accounting_dimension_fields(): | ||||
| 	dimensions_and_defaults = get_dimensions() | ||||
| 	if dimensions_and_defaults: | ||||
| 		for dimension in dimensions_and_defaults[0]: | ||||
| 			make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) | ||||
| 
 | ||||
| 
 | ||||
| def execute(): | ||||
| 	# create accounting dimension fields in Payment Ledger | ||||
| 	create_accounting_dimension_fields() | ||||
| 
 | ||||
| 	gl = qb.DocType("GL Entry") | ||||
| 	accounts = frappe.db.get_list( | ||||
| 		"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True | ||||
| 	) | ||||
| 	gl_entries = [] | ||||
| 	if accounts: | ||||
| 		# get all gl entries on receivable/payable accounts | ||||
| 		gl_entries = ( | ||||
| 			qb.from_(gl) | ||||
| 			.select("*") | ||||
| 			.where(gl.account.isin(accounts)) | ||||
| 			.where(gl.is_cancelled == 0) | ||||
| 			.run(as_dict=True) | ||||
| 		) | ||||
| 		if gl_entries: | ||||
| 			# create payment ledger entries for the accounts receivable/payable | ||||
| 			create_payment_ledger_entry(gl_entries, 0) | ||||
| @ -232,7 +232,7 @@ class SalesOrder(SellingController): | ||||
| 			update_coupon_code_count(self.coupon_code, "used") | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") | ||||
| 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") | ||||
| 		super(SalesOrder, self).on_cancel() | ||||
| 
 | ||||
| 		# Cannot cancel closed SO | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user