feat: payment ledger doctype created
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
new file mode 100644
index 0000000..5a7be8e
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
new file mode 100644
index 0000000..d961076
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
new file mode 100644
index 0000000..43e19f4
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
@@ -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()
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
new file mode 100644
index 0000000..f874b75
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -0,0 +1,215 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, 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.accounts.party import get_party_account
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+# class TestPaymentLedgerEntry(FrappeTestCase):
+class TestPaymentLedgerEntry(unittest.TestCase):
+ def setUp(self):
+ 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_create_all_types(self):
+ transaction_date = nowdate()
+ amount = 100
+ # full payment using PE
+ si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+ pe2 = get_payment_entry(si1.doctype, si1.name).save().submit()
+
+ # 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()
+
+ # 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()
+
+ # 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()
+
+ def test_dummy(self):
+ pass