From 163085f2018b4231d126031dbaa0eaa84715ce21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 May 2022 19:43:38 +0530 Subject: [PATCH] feat: payment ledger doctype created --- .../doctype/payment_ledger_entry/__init__.py | 0 .../payment_ledger_entry.js | 8 + .../payment_ledger_entry.json | 180 +++++++++++++++ .../payment_ledger_entry.py | 22 ++ .../test_payment_ledger_entry.py | 215 ++++++++++++++++++ 5 files changed, 425 insertions(+) create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/__init__.py create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py 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 0000000000..e69de29bb2 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 0000000000..5a7be8e5ab --- /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 0000000000..d96107678f --- /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 0000000000..43e19f4ae7 --- /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 0000000000..f874b75432 --- /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