From dc7162329594337ee5b869f42d44fb28d8eaf2e3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 10:26:42 +0530 Subject: [PATCH 01/29] feat: introduce unreconcile doctype --- .../unreconcile_payment_entries/__init__.py | 0 .../unreconcile_payment_entries.json | 71 ++++++++++++++++++ .../unreconcile_payment_entries.py | 9 +++ .../doctype/unreconcile_payments/__init__.py | 0 .../test_unreconcile_payments.py | 9 +++ .../unreconcile_payments.js | 8 +++ .../unreconcile_payments.json | 72 +++++++++++++++++++ .../unreconcile_payments.py | 9 +++ 8 files changed, 178 insertions(+) create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py b/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json new file mode 100644 index 00000000000..5beb39d0342 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-08-22 10:28:10.196712", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no", + "reference_type", + "reference_name", + "allocated_amount", + "unlinked" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Allocated Amount" + }, + { + "default": "0", + "fieldname": "unlinked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unlinked" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-22 11:22:20.381079", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payment Entries", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py new file mode 100644 index 00000000000..c41545c2685 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePaymentEntries(Document): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/unreconcile_payments/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py new file mode 100644 index 00000000000..85af5211aef --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestUnreconcilePayments(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js new file mode 100644 index 00000000000..d6670037d46 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Unreconcile Payments", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json new file mode 100644 index 00000000000..c182a63b654 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:UNREC-{#####}", + "creation": "2023-08-22 10:26:34.421423", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "entries", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Unreconcile Payments", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "entries", + "fieldtype": "Table", + "label": "Entries", + "options": "Unreconcile Payment Entries" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-08-22 11:07:03.854434", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payments", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py new file mode 100644 index 00000000000..96bcc009170 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePayments(Document): + pass From e48a90efe69f36dc455df3fefa8131384903e422 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 15:01:14 +0530 Subject: [PATCH 02/29] chore: working state on barebones functions --- .../unreconcile_payment_entries.json | 36 ++++++------------- .../unreconcile_payments.js | 25 ++++++++++--- .../unreconcile_payments.json | 22 +++++++++--- .../unreconcile_payments.py | 33 +++++++++++++++-- 4 files changed, 80 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 5beb39d0342..f70f4db2a8e 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,41 +6,18 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "voucher_type", - "voucher_no", - "reference_type", + "reference_doctype", "reference_name", "allocated_amount", "unlinked" ], "fields": [ - { - "fieldname": "voucher_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Voucher Type", - "options": "DocType" - }, - { - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "in_list_view": 1, - "label": "Voucher No", - "options": "voucher_type" - }, - { - "fieldname": "reference_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Reference Type", - "options": "DocType" - }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Reference Name", - "options": "reference_type" + "options": "reference_doctype" }, { "fieldname": "allocated_amount", @@ -54,12 +31,19 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Unlinked" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 11:22:20.381079", + "modified": "2023-08-22 15:00:33.203161", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index d6670037d46..03a8253dd2f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -1,8 +1,25 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Unreconcile Payments", { -// refresh(frm) { +frappe.ui.form.on("Unreconcile Payments", { + refresh(frm) { + frm.set_query("voucher_type", function() { + return { + filters: { + name: "Payment Entry" + } + } + }); -// }, -// }); + + frm.set_query("voucher_no", function(doc) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + }); + + }, +}); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index c182a63b654..f4b3cd70901 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -9,7 +9,9 @@ "engine": "InnoDB", "field_order": [ "company", - "entries", + "voucher_type", + "voucher_no", + "references", "amended_from" ], "fields": [ @@ -29,16 +31,28 @@ "options": "Company" }, { - "fieldname": "entries", + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "references", "fieldtype": "Table", - "label": "Entries", + "label": "References", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 11:07:03.854434", + "modified": "2023-08-22 14:11:13.073414", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 96bcc009170..df08d79f01f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -1,9 +1,38 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding + class UnreconcilePayments(Document): - pass + def before_save(self): + if self.voucher_type == "Payment Entry": + references = frappe.db.get_all( + "Payment Entry Reference", + filters={"docstatus": 1, "parent": self.voucher_no}, + fields=["reference_doctype", "reference_name", "allocated_amount"], + ) + + self.set("references", []) + for ref in references: + self.append("references", ref) + + def on_submit(self): + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( + self.voucher_type, + filters={"name": self.voucher_no}, + fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], + as_list=1, + )[0] + account = paid_from if payment_type == "Receive" else paid_to + + for ref in self.references: + doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + unlink_ref_doc_from_payment_entries(doc) + update_voucher_outstanding( + ref.reference_doctype, ref.reference_name, account, party_type, party + ) + frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) From 5114a9580db961a006d9b2f3c4dc08f207f374c7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 14:52:26 +0530 Subject: [PATCH 03/29] refactor: adding 'Get Allocations' button --- .../unreconcile_payment_entries.json | 5 +- .../unreconcile_payments.js | 16 +++++++ .../unreconcile_payments.json | 14 ++++-- .../unreconcile_payments.py | 47 +++++++++++++------ 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index f70f4db2a8e..c4afaa8bcac 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -30,7 +30,8 @@ "fieldname": "unlinked", "fieldtype": "Check", "in_list_view": 1, - "label": "Unlinked" + "label": "Unlinked", + "read_only": 1 }, { "fieldname": "reference_doctype", @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 15:00:33.203161", + "modified": "2023-08-24 14:48:10.018574", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index 03a8253dd2f..ef7c958113c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -22,4 +22,20 @@ frappe.ui.form.on("Unreconcile Payments", { }); }, + get_allocations: function(frm) { + frm.clear_table("allocations"); + frappe.call({ + method: "get_allocations_from_payment", + doc: frm.doc, + callback: function(r) { + if (r.message) { + r.message.forEach(x => { + frm.add_child("allocations", x) + }) + frm.refresh_fields(); + } + } + }) + + } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index f4b3cd70901..68af5dcc12a 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -11,7 +11,8 @@ "company", "voucher_type", "voucher_no", - "references", + "get_allocations", + "allocations", "amended_from" ], "fields": [ @@ -43,16 +44,21 @@ "options": "voucher_type" }, { - "fieldname": "references", + "fieldname": "get_allocations", + "fieldtype": "Button", + "label": "Get Allocations" + }, + { + "fieldname": "allocations", "fieldtype": "Table", - "label": "References", + "label": "Allocations", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 14:11:13.073414", + "modified": "2023-08-24 16:53:50.767700", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index df08d79f01f..ab2cc718ada 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,25 +2,44 @@ # For license information, please see license.txt import frappe +from frappe import qb from frappe.model.document import Document +from frappe.query_builder.functions import Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): - def before_save(self): - if self.voucher_type == "Payment Entry": - references = frappe.db.get_all( - "Payment Entry Reference", - filters={"docstatus": 1, "parent": self.voucher_no}, - fields=["reference_doctype", "reference_name", "allocated_amount"], - ) + # def validate(self): + # parent = set([alloc.parent for alloc in self.allocations]) + # if len(parent) != 1: + # pass - self.set("references", []) - for ref in references: - self.append("references", ref) + @frappe.whitelist() + def get_allocations_from_payment(self): + if self.voucher_type == "Payment Entry": + per = qb.DocType("Payment Entry Reference") + allocated_references = ( + qb.from_(per) + .select( + per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") + ) + .where((per.docstatus == 1) & (per.parent == self.voucher_no)) + .groupby(per.reference_name) + .run(as_dict=True) + ) + return allocated_references + + def add_references(self): + allocations = self.get_allocations_from_payment() + + for alloc in allocations: + self.append("allocations", alloc) def on_submit(self): + # todo: add more granular unlinking + # different amounts for same invoice should be individually unlinkable + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( self.voucher_type, filters={"name": self.voucher_no}, @@ -29,10 +48,10 @@ class UnreconcilePayments(Document): )[0] account = paid_from if payment_type == "Receive" else paid_to - for ref in self.references: - doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + for alloc in self.allocations: + doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - ref.reference_doctype, ref.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, account, party_type, party ) - frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) + frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From 0faffaa8db495f94d8bbd673faac5d4acdcc58a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 17:55:02 +0530 Subject: [PATCH 04/29] test: basic unreconcile function --- .../test_unreconcile_payments.py | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 85af5211aef..2bb8a54c350 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -1,9 +1,101 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +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.test.accounts_mixin import AccountsTestMixin -class TestUnreconcilePayments(FrappeTestCase): - pass +class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_unreconcile_invoice(self): + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=200, + save=True, + ) + + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + # Allocation payment against both invoices + pe.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + + pe.reload() + self.assertEqual(len(pe.references), 1) From fc6be5bfb9a2cf1a79d0150fc5867ba0cb988f64 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:29:50 +0530 Subject: [PATCH 05/29] feat: UI for unreconcile --- .../doctype/sales_invoice/sales_invoice.js | 44 +++++++++++++++++ .../unreconcile_payments.py | 49 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 642e99cd58a..fe931ee822c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -183,6 +183,50 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e }, __('Create')); } } + + if (doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name + }, + callback: function(r) { + if (r.message) { + me.frm.add_custom_button(__("Un-Reconcile"), function() { + me.unreconcile_prompt(); + }); + } + } + }); + } + } + + unreconcile_prompt() { + // get linked payments + let query_args = { + query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + filters: { + doctype: this.frm.doc.doctype, + docname: this.frm.doc.name + } + } + + new frappe.ui.form.MultiSelectDialog({ + doctype: "Payment Ledger Entry", + target: this.cur_frm, + setters: { }, + add_filters_group: 0, + date_field: "posting_date", + columns: ["voucher_type", "voucher_no", "allocated_amount"], + get_query() { + return query_args; + }, + action(selections) { + console.log(selections); + } + }); + } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ab2cc718ada..ed978cbc376 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,7 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -55,3 +55,50 @@ class UnreconcilePayments(Document): alloc.reference_doctype, alloc.reference_name, account, party_type, party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) + + +@frappe.whitelist() +def doc_has_payments(doctype, docname): + if doctype in ["Sales Invoice", "Purchase Invoice"]: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]}, + ) + else: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]}, + ) + + +@frappe.whitelist() +def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): + if filters.get("doctype") and filters.get("docname"): + _dt = filters.get("doctype") + _dn = filters.get("docname") + ple = qb.DocType("Payment Ledger Entry") + if _dt in ["Sales Invoice", "Purchase Invoice"]: + res = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .groupby(ple.against_voucher_no) + .run(as_dict=True) + ) + return res + else: + return frappe.db.get_all( + "Payment Ledger Entry", + filters={ + "delinked": 0, + "voucher_no": _dn, + "against_voucher_no": ["!=", _dn], + "amount": ["<", 0], + }, + group_by="against_voucher_no", + fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + ) From 41eb2c9f5a2aeadab0fd2401cca86bd8302f7eb2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:45:18 +0530 Subject: [PATCH 06/29] feat: filter on voucher no --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index fe931ee822c..704381817e9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -219,6 +219,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e add_filters_group: 0, date_field: "posting_date", columns: ["voucher_type", "voucher_no", "allocated_amount"], + primary_action_label: "Un-Reconcile", + title: "Un-Reconcile Payments", get_query() { return query_args; }, diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ed978cbc376..dfd2d29e0f4 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,6 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -78,6 +79,11 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt _dn = filters.get("docname") ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: + criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] + + if txt: + criteria.append(ple.voucher_no.like(f"%{txt}%")) + res = ( qb.from_(ple) .select( @@ -85,7 +91,7 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) .run(as_dict=True) ) From fbdfb8151c1f79fcc9b835a4ccc5c954e65b743f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 16:27:29 +0530 Subject: [PATCH 07/29] chore: delete references upon parent deletion --- erpnext/controllers/accounts_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9725c257296..9c502501a0d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -218,6 +218,11 @@ class AccountsController(TransactionBase): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() + upe = frappe.qb.DocType("UnReconcile Payment Entries") + frappe.qb.from_(upe).delete().where( + (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) + ).run() + # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): ple = frappe.qb.DocType("Payment Ledger Entry") From 42df0d3d6729a57953bc9cb0ef8622eae34829a0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:36:12 +0530 Subject: [PATCH 08/29] refactor: remove references using framework --- .../doctype/sales_invoice/sales_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.py | 2 ++ erpnext/controllers/accounts_controller.py | 27 ++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 704381817e9..7b69f018d00 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index fba2fa7552e..7bdb2b49cea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -388,6 +388,8 @@ class SalesInvoice(SellingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", "Payment Ledger Entry", "Serial and Batch Bundle", ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9c502501a0d..7c9531877b6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -211,6 +211,28 @@ class AccountsController(TransactionBase): def before_cancel(self): validate_einvoice_fields(self) + def _remove_references_in_unreconcile(self): + upe = frappe.qb.DocType("UnReconcile Payment Entries") + rows = ( + frappe.qb.from_(upe) + .select(upe.name, upe.parent) + .where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name)) + .run(as_dict=True) + ) + + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) + + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + def on_trash(self): # delete references in 'Repost Payment Ledger' rpi = frappe.qb.DocType("Repost Payment Ledger Items") @@ -218,10 +240,7 @@ class AccountsController(TransactionBase): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() - upe = frappe.qb.DocType("UnReconcile Payment Entries") - frappe.qb.from_(upe).delete().where( - (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) - ).run() + self._remove_references_in_unreconcile() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): From 489a545bbb1a814cb18164321e10a0d17042272c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:43:05 +0530 Subject: [PATCH 09/29] chore: track changes --- .../doctype/unreconcile_payments/unreconcile_payments.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index 68af5dcc12a..f29e61b6ef6 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-24 16:53:50.767700", + "modified": "2023-08-28 17:42:50.261377", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", @@ -88,5 +88,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file From 6bbe47c6714546114d77e34cbefbc7f30227050f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:49:09 +0530 Subject: [PATCH 10/29] chore: delete unreoncile doc upon parent doc deletion --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 2 ++ erpnext/controllers/accounts_controller.py | 29 ++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9a0adf5815d..5f7e96f7f1c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2c2efc06455..45de0acc00a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -149,6 +149,8 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7c9531877b6..5631fca4280 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -220,18 +220,27 @@ class AccountsController(TransactionBase): .run(as_dict=True) ) - references_map = frappe._dict() - for x in rows: - references_map.setdefault(x.parent, []).append(x.name) + if rows: + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) - for doc, rows in references_map.items(): - unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) - for row in rows: - unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) - unreconcile_doc.flags.ignore_validate_update_after_submit = True - unreconcile_doc.flags.ignore_links = True - unreconcile_doc.save(ignore_permissions=True) + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + + # delete docs upon parent doc deletion + unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) + for x in unreconcile_docs: + _doc = frappe.get_doc("Unreconcile Payments", x.name) + if _doc.docstatus == 1: + _doc.cancel() + _doc.delete() def on_trash(self): # delete references in 'Repost Payment Ledger' From 58dc0e52e197e89653f0778fc434d86611616808 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 11:27:16 +0530 Subject: [PATCH 11/29] refactor: add UI elements --- .../doctype/sales_invoice/sales_invoice.js | 71 +++++++++++++------ .../unreconcile_payments.py | 37 +++++++--- 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7b69f018d00..b95bb00dd15 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -203,32 +203,57 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } unreconcile_prompt() { - // get linked payments - let query_args = { - query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - filters: { - doctype: this.frm.doc.doctype, - docname: this.frm.doc.name - } - } - - new frappe.ui.form.MultiSelectDialog({ - doctype: "Payment Ledger Entry", - target: this.cur_frm, - setters: { }, - add_filters_group: 0, - date_field: "posting_date", - columns: ["voucher_type", "voucher_no", "allocated_amount"], - primary_action_label: "Un-Reconcile", - title: "Un-Reconcile Payments", - get_query() { - return query_args; + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, }, - action(selections) { - console.log(selections); + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": this.frm.doc.company, + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } } }); - } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index dfd2d29e0f4..cced2b3de49 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -73,20 +73,25 @@ def doc_has_payments(doctype, docname): @frappe.whitelist() -def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): - if filters.get("doctype") and filters.get("docname"): - _dt = filters.get("doctype") - _dn = filters.get("docname") +def get_linked_payments_for_doc( + company: str = None, doctype: str = None, docname: str = None +) -> list: + if company and doctype and docname: + _dt = doctype + _dn = docname ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: - criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] - - if txt: - criteria.append(ple.voucher_no.like(f"%{txt}%")) + criteria = [ + (ple.delinked == 0), + (ple.against_voucher_no == _dn), + (ple.amount < 0), + (ple.company == company), + ] res = ( qb.from_(ple) .select( + ple.company, ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), @@ -108,3 +113,19 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt group_by="against_voucher_no", fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], ) + return [] + + +@frappe.whitelist() +def create_unreconcile_doc_for_selection( + company: str = None, dt: str = None, dn: str = None, selections: list = None +): + if selections: + # assuming each row is a unique voucher + for row in selections: + unrecon = frappe.new_doc("Unreconcile Payments") + unrecon.company = company + unrecon.voucher_type = dt + unrecon.voucher_type = dn + unrecon.add_references() + # remove unselected references From 5981c7e0ad1d80236654ceb4214b97a178fe5a05 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:19:26 +0530 Subject: [PATCH 12/29] chore: move dialog building function to `utils.js` file --- .../doctype/sales_invoice/sales_invoice.js | 55 +----------------- erpnext/public/js/utils.js | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b95bb00dd15..6856d252926 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -194,7 +194,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e callback: function(r) { if (r.message) { me.frm.add_custom_button(__("Un-Reconcile"), function() { - me.unreconcile_prompt(); + erpnext.utils.build_unreconcile_dialog(cur_frm); }); } } @@ -202,59 +202,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - unreconcile_prompt() { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": this.frm.doc.company, - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - - d.show(); - } - } - }); - } make_maintenance_schedule() { frappe.model.open_mapped_doc({ diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 89750f8446c..d3442af9996 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -769,6 +769,62 @@ erpnext.utils.update_child_items = function(opts) { dialog.show(); } +erpnext.utils.build_unreconcile_dialog = function(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } +} + erpnext.utils.map_current_doc = function(opts) { function _map() { if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { @@ -1097,4 +1153,4 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) { $btn.on("click", function() { context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true); }); -} \ No newline at end of file +} From 25fe75218578a44302f1335f8db0caa17d4d7608 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:46:29 +0530 Subject: [PATCH 13/29] chore: move functions to a separate file in utils --- .../doctype/payment_entry/payment_entry.js | 1 + .../doctype/sales_invoice/sales_invoice.js | 17 +-- .../unreconcile_payments.py | 19 +++- erpnext/public/js/erpnext.bundle.js | 3 +- erpnext/public/js/utils.js | 53 --------- erpnext/public/js/utils/unreconcile.js | 106 ++++++++++++++++++ 6 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 erpnext/public/js/utils/unreconcile.js diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 5f7e96f7f1c..794a4ef1bc0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -154,6 +154,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 6856d252926..d4d923902f1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -184,22 +184,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - if (doc.docstatus == 1) { - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", - "args": { - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - me.frm.add_custom_button(__("Un-Reconcile"), function() { - erpnext.utils.build_unreconcile_dialog(cur_frm); - }); - } - } - }); - } + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index cced2b3de49..c80365b0ef0 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -117,15 +117,22 @@ def get_linked_payments_for_doc( @frappe.whitelist() -def create_unreconcile_doc_for_selection( - company: str = None, dt: str = None, dn: str = None, selections: list = None -): +def create_unreconcile_doc_for_selection(selections=None): if selections: + selections = frappe.json.loads(selections) # assuming each row is a unique voucher for row in selections: unrecon = frappe.new_doc("Unreconcile Payments") - unrecon.company = company - unrecon.voucher_type = dt - unrecon.voucher_type = dn + unrecon.company = row.get("company") + unrecon.voucher_type = row.get("voucher_type") + unrecon.voucher_no = row.get("voucher_no") unrecon.add_references() + # remove unselected references + unrecon.allocations = [ + x + for x in unrecon.allocations + if x.reference_doctype == row.get("against_voucher_type") + and x.reference_name == row.get("against_voucher_no") + ] + unrecon.save().submit() diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 966a9e1f9b3..0e1b23b0eae 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -16,7 +16,8 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; -import "./utils/ledger_preview.js" +import "./utils/ledger_preview.js"; +import "./utils/unreconcile.js"; import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index d3442af9996..d435711cf52 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -769,61 +769,8 @@ erpnext.utils.update_child_items = function(opts) { dialog.show(); } -erpnext.utils.build_unreconcile_dialog = function(frm) { - if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": frm.doc.company, - "doctype": frm.doc.doctype, - "docname": frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - - d.show(); - } - } - }); - } -} erpnext.utils.map_current_doc = function(opts) { function _map() { diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js new file mode 100644 index 00000000000..509cd394100 --- /dev/null +++ b/erpnext/public/js/utils/unreconcile.js @@ -0,0 +1,106 @@ +frappe.provide('erpnext.accounts'); + +erpnext.accounts.unreconcile_payments = { + add_unreconcile_btn(frm) { + if (frm.doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + frm.add_custom_button(__("Un-Reconcile"), function() { + erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); + }); + } + } + }); + } + }, + + build_unreconcile_dialog(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + let selection_map = selected_allocations.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + + }); + + erpnext.utils.create_unreconcile_docs(selection_map); + d.hide(); + } + + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } + }, + + create_unreconcile_docs(selection_map) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection", + "args": { + "selections": selection_map + }, + }); + } + + + +} From 1981f3837a10b5c0c2298a682190e0e6689a8b19 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 15:15:14 +0530 Subject: [PATCH 14/29] chore: fetch logic for payment entry --- .../unreconcile_payments.py | 31 +++++++----- erpnext/public/js/utils/unreconcile.js | 48 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index c80365b0ef0..b6dd363cea5 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -82,10 +82,10 @@ def get_linked_payments_for_doc( ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: criteria = [ + (ple.company == company), (ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0), - (ple.company == company), ] res = ( @@ -102,17 +102,26 @@ def get_linked_payments_for_doc( ) return res else: - return frappe.db.get_all( - "Payment Ledger Entry", - filters={ - "delinked": 0, - "voucher_no": _dn, - "against_voucher_no": ["!=", _dn], - "amount": ["<", 0], - }, - group_by="against_voucher_no", - fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + criteria = [ + (ple.company == company), + (ple.delinked == 0), + (ple.voucher_no == _dn), + (ple.against_voucher_no != _dn), + ] + + query = ( + qb.from_(ple) + .select( + ple.company, + ple.against_voucher_type.as_("voucher_type"), + ple.against_voucher_no.as_("voucher_no"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where(Criterion.all(criteria)) + .groupby(ple.against_voucher_no) ) + res = query.run(as_dict=True) + return res return [] diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 509cd394100..46555fe2a2b 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -20,6 +20,34 @@ erpnext.accounts.unreconcile_payments = { } }, + build_selection_map(frm, selections) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + let selection_map = []; + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + }); + } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: frm.doc.doctype, + voucher_no: frm.doc.name, + against_voucher_type: elem.voucher_type, + against_voucher_no: elem.voucher_no, + }; + }); + } + return selection_map; + }, + build_unreconcile_dialog(frm) { if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { let child_table_fields = [ @@ -61,23 +89,9 @@ erpnext.accounts.unreconcile_payments = { let selected_allocations = values.allocations.filter(x=>x.__checked); if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that creates unreconcile doc for each row - if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { - let selection_map = selected_allocations.map(function(elem) { - return { - company: elem.company, - voucher_type: elem.voucher_type, - voucher_no: elem.voucher_no, - against_voucher_type: frm.doc.doctype, - against_voucher_no: frm.doc.name - }; - - }); - - erpnext.utils.create_unreconcile_docs(selection_map); - d.hide(); - } + let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations); + erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map); + d.hide(); } else { frappe.msgprint("No Selection"); From 69683776a5e46c32ba16664264f3aa9bc09a03f5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:45:17 +0530 Subject: [PATCH 15/29] chore: code cleanup --- .../doctype/unreconcile_payments/unreconcile_payments.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index b6dd363cea5..01f910e5646 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -11,11 +11,6 @@ from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_v class UnreconcilePayments(Document): - # def validate(self): - # parent = set([alloc.parent for alloc in self.allocations]) - # if len(parent) != 1: - # pass - @frappe.whitelist() def get_allocations_from_payment(self): if self.voucher_type == "Payment Entry": From 0ccb6d8242c8bcb44457745553124629e4dc5434 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:49:21 +0530 Subject: [PATCH 16/29] chore: rename and add trigger in journal entry --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- erpnext/public/js/utils/unreconcile.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 35a378856b0..cdd1203d49a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -50,6 +50,8 @@ frappe.ui.form.on("Journal Entry", { frm.trigger("make_inter_company_journal_entry"); }, __('Make')); } + + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); }, make_inter_company_journal_entry: function(frm) { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 01f910e5646..9b80c0a3f85 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -54,7 +54,7 @@ class UnreconcilePayments(Document): @frappe.whitelist() -def doc_has_payments(doctype, docname): +def doc_has_references(doctype, docname): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 46555fe2a2b..df07643bb7c 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -4,7 +4,7 @@ erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { "doctype": frm.doc.doctype, "docname": frm.doc.name From cce96669f0b651795522ac350319993df1d482e9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:02:47 +0530 Subject: [PATCH 17/29] refactor: modularisation and group by voucher_no --- .../unreconcile_payments.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 9b80c0a3f85..8aef772ad58 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -13,6 +13,7 @@ from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_v class UnreconcilePayments(Document): @frappe.whitelist() def get_allocations_from_payment(self): + allocated_references = [] if self.voucher_type == "Payment Entry": per = qb.DocType("Payment Entry Reference") allocated_references = ( @@ -24,7 +25,19 @@ class UnreconcilePayments(Document): .groupby(per.reference_name) .run(as_dict=True) ) - return allocated_references + elif self.voucher_type == "Journal Entry": + jea = qb.DocType("Journal Entry Account") + allocated_references = ( + qb.from_(jea) + .select( + jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ) + .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) + .groupby(jea.reference_name) + .run(as_dict=True) + ) + + return allocated_references def add_references(self): allocations = self.get_allocations_from_payment() @@ -92,7 +105,7 @@ def get_linked_payments_for_doc( Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) .where(Criterion.all(criteria)) - .groupby(ple.against_voucher_no) + .groupby(ple.voucher_no, ple.against_voucher_no) .run(as_dict=True) ) return res From 285963acdba73bfdb0f9e5d7f4fac3d765b282d6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:43:00 +0530 Subject: [PATCH 18/29] feat: unreconcile support for journal entry --- .../unreconcile_payments.js | 2 +- .../unreconcile_payments.py | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index ef7c958113c..c522567637f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -6,7 +6,7 @@ frappe.ui.form.on("Unreconcile Payments", { frm.set_query("voucher_type", function() { return { filters: { - name: "Payment Entry" + name: ["in", ["Payment Entry", "Journal Entry"]] } } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 8aef772ad58..a32313f4a5c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,15 +2,21 @@ # For license information, please see license.txt import frappe -from frappe import qb +from frappe import _, qb from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum +from frappe.utils.data import comma_and from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): + def validate(self): + self.supported_types = ["Payment Entry", "Journal Entry"] + if not self.voucher_type in self.supported_types: + frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types))) + @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] @@ -26,14 +32,24 @@ class UnreconcilePayments(Document): .run(as_dict=True) ) elif self.voucher_type == "Journal Entry": - jea = qb.DocType("Journal Entry Account") + # for journals, using payment ledger to fetch allocation. + # this way we can avoid vaildating account type and reference details individually on child table + + ple = qb.DocType("Payment Ledger Entry") allocated_references = ( - qb.from_(jea) + qb.from_(ple) .select( - jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) - .groupby(jea.reference_name) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) + ) + .groupby(ple.against_voucher_type, ple.against_voucher_no) .run(as_dict=True) ) From de910ab152801dcfa18fe72d45853680716630b6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 11:02:03 +0530 Subject: [PATCH 19/29] refactor: single fetch and unlinking logic for JE and PE --- .../unreconcile_payment_entries.json | 20 ++++++- .../unreconcile_payments.py | 58 ++++++------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index c4afaa8bcac..955c3bbe031 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,6 +6,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "account", + "party_type", + "party", "reference_doctype", "reference_name", "allocated_amount", @@ -39,12 +42,27 @@ "in_list_view": 1, "label": "Reference Type", "options": "DocType" + }, + { + "fieldname": "account", + "fieldtype": "Data", + "label": "Account" + }, + { + "fieldname": "party_type", + "fieldtype": "Data", + "label": "Party Type" + }, + { + "fieldname": "party", + "fieldtype": "Data", + "label": "Party" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-24 14:48:10.018574", + "modified": "2023-08-30 10:58:45.322668", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index a32313f4a5c..1688b6e4984 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -20,38 +20,26 @@ class UnreconcilePayments(Document): @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] - if self.voucher_type == "Payment Entry": - per = qb.DocType("Payment Entry Reference") - allocated_references = ( - qb.from_(per) - .select( - per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") - ) - .where((per.docstatus == 1) & (per.parent == self.voucher_no)) - .groupby(per.reference_name) - .run(as_dict=True) + ple = qb.DocType("Payment Ledger Entry") + allocated_references = ( + qb.from_(ple) + .select( + ple.account, + ple.party_type, + ple.party, + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - elif self.voucher_type == "Journal Entry": - # for journals, using payment ledger to fetch allocation. - # this way we can avoid vaildating account type and reference details individually on child table - - ple = qb.DocType("Payment Ledger Entry") - allocated_references = ( - qb.from_(ple) - .select( - ple.against_voucher_type.as_("reference_doctype"), - ple.against_voucher_no.as_("reference_name"), - Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), - ) - .where( - (ple.docstatus == 1) - & (ple.voucher_type == self.voucher_type) - & (ple.voucher_no == self.voucher_no) - & (ple.voucher_no != ple.against_voucher_no) - ) - .groupby(ple.against_voucher_type, ple.against_voucher_no) - .run(as_dict=True) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) ) + .groupby(ple.against_voucher_type, ple.against_voucher_no) + .run(as_dict=True) + ) return allocated_references @@ -65,19 +53,11 @@ class UnreconcilePayments(Document): # todo: add more granular unlinking # different amounts for same invoice should be individually unlinkable - payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( - self.voucher_type, - filters={"name": self.voucher_no}, - fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], - as_list=1, - )[0] - account = paid_from if payment_type == "Receive" else paid_to - for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - alloc.reference_doctype, alloc.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From 0130aea2aa1ac93cd790af3652ea7b43871c23c8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 13:25:23 +0530 Subject: [PATCH 20/29] refactor: convert raw sql to query_builder --- erpnext/accounts/utils.py | 119 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index eed74a5f017..5c9b0dd827b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -705,72 +705,87 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - - frappe.db.sql( - """update `tabGL Entry` - set against_voucher_type=null, against_voucher=null, - modified=%s, modified_by=%s - where against_voucher_type=%s and against_voucher=%s - and voucher_no != ifnull(against_voucher, '')""", - (now(), frappe.session.user, ref_doc.doctype, ref_doc.name), - ) +def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): + # General Ledger + gle = qb.DocType("GL Entry") + qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( + gle.modified, now() + ).set(gle.modified_by, frappe.session.user).where( + (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) + ).run() + # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( ple.against_voucher_no, ple.voucher_no ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_doc.doctype) - & (ple.against_voucher_no == ref_doc.name) - & (ple.delinked == 0) + (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) ).run() + +def remove_ref_from_advance_section(ref_doc: object = None): if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) + adv_type = qb.DocType(f"{ref_doc.doctype} Advance") + qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() - frappe.db.sql( - """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name - ) + +def unlink_ref_doc_from_payment_entries(ref_doc): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) def remove_ref_doc_link_from_jv(ref_type, ref_no): - linked_jv = frappe.db.sql_list( - """select parent from `tabJournal Entry Account` - where reference_type=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + jea = qb.DocType("Journal Entry Account") + + linked_jv = ( + qb.from_(jea) + .select(jea.parent) + .select( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_jv = convert_to_list(linked_jv) if linked_jv: - frappe.db.sql( - """update `tabJournal Entry Account` - set reference_type=null, reference_name = null, - modified=%s, modified_by=%s - where reference_type=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( + jea.modified, now() + ).set(jea.modified_by, frappe.session.user).where( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) + ).run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) +def convert_to_list(result): + """ + Convert tuple to list + """ + return [x[0] for x in result] + + def remove_ref_doc_link_from_pe(ref_type, ref_no): - linked_pe = frappe.db.sql_list( - """select parent from `tabPayment Entry Reference` - where reference_doctype=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + per = qb.DocType("Payment Entry Reference") + pay = qb.DocType("Payment Entry") + + linked_pe = ( + qb.from_(per) + .select(per.parent) + .where( + (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_pe = convert_to_list(linked_pe) if linked_pe: - frappe.db.sql( - """update `tabPayment Entry Reference` - set allocated_amount=0, modified=%s, modified_by=%s - where reference_doctype=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( + per.modified_by, frappe.session.user + ).where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ).run() for pe in linked_pe: try: @@ -785,19 +800,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): msg += _("Please cancel payment entry manually first") frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) - frappe.db.sql( - """update `tabPayment Entry` set total_allocated_amount=%s, - base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s - where name=%s""", - ( - pe_doc.total_allocated_amount, - pe_doc.base_total_allocated_amount, - pe_doc.unallocated_amount, - now(), - frappe.session.user, - pe, - ), - ) + qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set( + pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount + ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( + pay.modified_by, frappe.session.user + ).where( + pay.name == pe + ).run() frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) From b4dc2bdf28bf9c1c7043750a4c91d786c230ea6a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:30:14 +0530 Subject: [PATCH 21/29] chore: type info --- .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 1688b6e4984..304ccccb089 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -63,7 +63,7 @@ class UnreconcilePayments(Document): @frappe.whitelist() -def doc_has_references(doctype, docname): +def doc_has_references(doctype: str = None, docname: str = None): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", From 9b6eac23b6ad38323c68f42fdcf1a2f8916705a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:44:25 +0530 Subject: [PATCH 22/29] refactor: unlink individual vouchers from payments --- .../unreconcile_payments.py | 2 +- erpnext/accounts/utils.py | 101 +++++++++++++----- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 304ccccb089..5161a928237 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -55,7 +55,7 @@ class UnreconcilePayments(Document): for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) - unlink_ref_doc_from_payment_entries(doc) + unlink_ref_doc_from_payment_entries(doc, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5c9b0dd827b..0a5c5b981ad 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -705,38 +705,62 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): +def update_accounting_ledgers_after_reference_removal( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): # General Ledger gle = qb.DocType("GL Entry") - qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( - gle.modified, now() - ).set(gle.modified_by, frappe.session.user).where( - (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) - ).run() + gle_update_query = ( + qb.update(gle) + .set(gle.against_voucher_type, None) + .set(gle.against_voucher, None) + .set(gle.modified, now()) + .set(gle.modified_by, frappe.session.user) + .where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no)) + ) + + if payment_name: + gle_update_query = gle_update_query.where(gle.voucher_no == payment_name) + gle_update_query.run() # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( - ple.against_voucher_no, ple.voucher_no - ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) - ).run() + ple_update_query = ( + qb.update(ple) + .set(ple.against_voucher_type, ple.voucher_type) + .set(ple.against_voucher_no, ple.voucher_no) + .set(ple.modified, now()) + .set(ple.modified_by, frappe.session.user) + .where( + (ple.against_voucher_type == ref_type) + & (ple.against_voucher_no == ref_no) + & (ple.delinked == 0) + ) + ) + + if payment_name: + ple_update_query = ple_update_query.where(ple.voucher_no == payment_name) + ple_update_query.run() def remove_ref_from_advance_section(ref_doc: object = None): + # TODO: this might need some testing if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) adv_type = qb.DocType(f"{ref_doc.doctype} Advance") qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) +def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_from_advance_section(ref_doc) -def remove_ref_doc_link_from_jv(ref_type, ref_no): +def remove_ref_doc_link_from_jv( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): jea = qb.DocType("Journal Entry Account") linked_jv = ( @@ -748,13 +772,23 @@ def remove_ref_doc_link_from_jv(ref_type, ref_no): .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) + # remove reference only from specified payment + linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv if linked_jv: - qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( - jea.modified, now() - ).set(jea.modified_by, frappe.session.user).where( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) - ).run() + update_query = ( + qb.update(jea) + .set(jea.reference_type, None) + .set(jea.reference_name, None) + .set(jea.modified, now()) + .set(jea.modified_by, frappe.session.user) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no)) + ) + + if payment_name: + update_query = update_query.where(jea.parent == payment_name) + + update_query.run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) @@ -766,7 +800,9 @@ def convert_to_list(result): return [x[0] for x in result] -def remove_ref_doc_link_from_pe(ref_type, ref_no): +def remove_ref_doc_link_from_pe( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") @@ -779,13 +815,24 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): .run(as_list=1) ) linked_pe = convert_to_list(linked_pe) + # remove reference only from specified payment + linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe if linked_pe: - qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( - per.modified_by, frappe.session.user - ).where( - (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) - ).run() + update_query = ( + qb.update(per) + .set(per.allocated_amount, 0) + .set(per.modified, now()) + .set(per.modified_by, frappe.session.user) + .where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ) + ) + + if payment_name: + update_query = update_query.where(per.parent == payment_name) + + update_query.run() for pe in linked_pe: try: From 67980188a7a673d42848005b5b1ebbad9a6c98df Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 17:03:02 +0530 Subject: [PATCH 23/29] test: more granular unreconciliation --- .../test_unreconcile_payments.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 2bb8a54c350..924a950c4fe 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -99,3 +99,112 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): pe.reload() self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + def test_02_unreconcile_one_payment_from_multi_payments(self): + """ + Scenario: 2 payments, both split against 2 invoices + Unreconcile only one payment from one invoice + """ + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe1.save().submit() + + pe2 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe2.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + + pe1.reload() + pe2.reload() + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) From 9a1588f1cccc5336ca7a7f45be66f84ab3dc1e06 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 20:50:16 +0530 Subject: [PATCH 24/29] fix: typo in doctype name and qb --- erpnext/accounts/utils.py | 4 +--- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0a5c5b981ad..f4d28c699bb 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -766,9 +766,7 @@ def remove_ref_doc_link_from_jv( linked_jv = ( qb.from_(jea) .select(jea.parent) - .select( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) - ) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2))) .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5631fca4280..df0d0c5f3fe 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -212,7 +212,7 @@ class AccountsController(TransactionBase): validate_einvoice_fields(self) def _remove_references_in_unreconcile(self): - upe = frappe.qb.DocType("UnReconcile Payment Entries") + upe = frappe.qb.DocType("Unreconcile Payment Entries") rows = ( frappe.qb.from_(upe) .select(upe.name, upe.parent) From 6fd1c1bca263b05cf035c97fcf56d209c137b61b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 08:53:10 +0530 Subject: [PATCH 25/29] refactor: display allocated amount in account currency with symbol --- .../unreconcile_payment_entries.json | 15 ++++++++++++--- .../unreconcile_payments/unreconcile_payments.py | 2 ++ erpnext/public/js/utils/unreconcile.js | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 955c3bbe031..42da669e650 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -12,6 +12,7 @@ "reference_doctype", "reference_name", "allocated_amount", + "account_currency", "unlinked" ], "fields": [ @@ -24,9 +25,10 @@ }, { "fieldname": "allocated_amount", - "fieldtype": "Int", + "fieldtype": "Currency", "in_list_view": 1, - "label": "Allocated Amount" + "label": "Allocated Amount", + "options": "account_currency" }, { "default": "0", @@ -57,12 +59,19 @@ "fieldname": "party", "fieldtype": "Data", "label": "Party" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-30 10:58:45.322668", + "modified": "2023-09-05 09:33:28.620149", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 5161a928237..25f85db71f7 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -30,6 +30,7 @@ class UnreconcilePayments(Document): ple.against_voucher_type.as_("reference_doctype"), ple.against_voucher_no.as_("reference_name"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where( (ple.docstatus == 1) @@ -99,6 +100,7 @@ def get_linked_payments_for_doc( ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index df07643bb7c..cd44f3578b0 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -53,7 +53,8 @@ erpnext.accounts.unreconcile_payments = { let child_table_fields = [ { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"}, + { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1}, ] let unreconcile_dialog_fields = [ { @@ -83,7 +84,7 @@ erpnext.accounts.unreconcile_payments = { title: 'Un-Reconcile Allocations', fields: unreconcile_dialog_fields, size: 'large', - cannot_add_rows: 1, + cannot_add_rows: true, primary_action_label: 'Un-Reconcile', primary_action(values) { From 5dbcf7d2b94dae06ef7fc31b3142606d65611bff Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:52:36 +0530 Subject: [PATCH 26/29] refactor: only cancel specific gain/loss je --- erpnext/accounts/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f4d28c699bb..4f3ea610b74 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -675,7 +675,9 @@ def update_reference_in_payment_entry( payment_entry.save(ignore_permissions=True) -def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: +def cancel_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None +) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ @@ -702,7 +704,18 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: as_list=1, ) for doc in gain_loss_journals: - frappe.get_doc("Journal Entry", doc[0]).cancel() + gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn + gain_loss_je.cancel() + else: + gain_loss_je.cancel() def update_accounting_ledgers_after_reference_removal( From 1d93d66c30e69bfcb277123462bf822aa3c3c1d4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:59:34 +0530 Subject: [PATCH 27/29] refactor: cancel gain/loss JE on multi currency transactions --- .../unreconcile_payments/unreconcile_payments.py | 13 +++++++++---- erpnext/public/js/utils/unreconcile.js | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 25f85db71f7..4f9fb50d463 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -8,7 +8,11 @@ from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum from frappe.utils.data import comma_and -from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + update_voucher_outstanding, +) class UnreconcilePayments(Document): @@ -51,12 +55,11 @@ class UnreconcilePayments(Document): self.append("allocations", alloc) def on_submit(self): - # todo: add more granular unlinking - # different amounts for same invoice should be individually unlinkable - + # todo: more granular unreconciliation for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc, self.voucher_no) + cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) @@ -104,6 +107,7 @@ def get_linked_payments_for_doc( ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) + .having(qb.Field("allocated_amount") > 0) .run(as_dict=True) ) return res @@ -122,6 +126,7 @@ def get_linked_payments_for_doc( ple.against_voucher_type.as_("voucher_type"), ple.against_voucher_no.as_("voucher_no"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index cd44f3578b0..acc77a64b01 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -3,6 +3,12 @@ frappe.provide('erpnext.accounts'); erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { + if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) + || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype) + ) { + return; + } + frappe.call({ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { From 5c09fdf9419e301ffbf3787db02d689758f4757e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Sep 2023 21:43:23 +0530 Subject: [PATCH 28/29] refactor(test): more modularization --- .../test_unreconcile_payments.py | 108 +++++------------- 1 file changed, 30 insertions(+), 78 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 924a950c4fe..3d7c6cbe321 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -20,20 +20,8 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def test_01_unreconcile_invoice(self): - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( + def create_sales_invoice(self): + si = create_sales_invoice( item=self.item, company=self.company, customer=self.customer, @@ -44,7 +32,9 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): rate=100, price_list_rate=100, ) + return si + def create_payment_entry(self): pe = create_payment_entry( company=self.company, payment_type="Receive", @@ -55,7 +45,13 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): paid_amount=200, save=True, ) + return pe + def test_01_unreconcile_invoice(self): + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + + pe = self.create_payment_entry() pe.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, @@ -68,10 +64,10 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): pe.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 0) self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(pe.unallocated_amount, 0) unreconcile = frappe.get_doc( { @@ -92,54 +88,22 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 100) self.assertEqual(si2.outstanding_amount, 0) - - pe.reload() self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 100) def test_02_unreconcile_one_payment_from_multi_payments(self): """ - Scenario: 2 payments, both split against 2 invoices + Scenario: 2 payments, both split against 2 different invoices Unreconcile only one payment from one invoice """ - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - pe1 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + pe1 = self.create_payment_entry() + pe1.paid_amount = 100 + # Allocate payment against both invoices pe1.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -148,19 +112,11 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe1.save().submit() - pe2 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + pe2 = self.create_payment_entry() + pe2.paid_amount = 100 + # Allocate payment against both invoices pe2.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -169,14 +125,14 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe2.save().submit() - # Assert outstanding - si1.reload() - si2.reload() - self.assertEqual(si1.outstanding_amount, 0) - self.assertEqual(si2.outstanding_amount, 0) + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 0.0) + self.assertEqual(si2.outstanding_amount, 0.0) + self.assertEqual(pe1.unallocated_amount, 0.0) + self.assertEqual(pe2.unallocated_amount, 0.0) unreconcile = frappe.get_doc( { @@ -196,14 +152,10 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile.remove(x) unreconcile.save().submit() - # Assert outstanding - si1.reload() - si2.reload() + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] self.assertEqual(si1.outstanding_amount, 50) self.assertEqual(si2.outstanding_amount, 0) - - pe1.reload() - pe2.reload() self.assertEqual(len(pe1.references), 2) self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) From d3987757151949a6ad37e906f3a14d0863307b97 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 9 Sep 2023 07:24:56 +0530 Subject: [PATCH 29/29] test: multi currency invoice unreconciliation exchange gain/loss associated with the unreconcile invoice should be cancelled as well --- .../test_unreconcile_payments.py | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 3d7c6cbe321..78e04bff819 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -14,13 +14,14 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() + self.create_usd_receivable_account() self.create_item() self.clear_old_entries() def tearDown(self): frappe.db.rollback() - def create_sales_invoice(self): + def create_sales_invoice(self, do_not_submit=False): si = create_sales_invoice( item=self.item, company=self.company, @@ -31,6 +32,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): cost_center=self.cost_center, rate=100, price_list_rate=100, + do_not_submit=do_not_submit, ) return si @@ -160,3 +162,155 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) self.assertEqual(pe2.unallocated_amount, 50) + + def test_03_unreconciliation_on_multi_currency_invoice(self): + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe = self.create_payment_entry() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = 75 + pe.received_amount = 75 * 200 + pe.save() + # Allocate payment against both invoices + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + pe.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe]] + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + # Exc gain/loss JE should've been cancelled as well + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 0, + ) + + def test_04_unreconciliation_on_multi_currency_invoice(self): + """ + 2 payments split against 2 foreign currency invoices + """ + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe1 = self.create_payment_entry() + pe1.paid_from = self.debtors_usd + pe1.paid_from_account_currency = "USD" + pe1.source_exchange_rate = 75 + pe1.received_amount = 75 * 100 + pe1.save() + # Allocate payment against both invoices + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe1.save().submit() + + pe2 = self.create_payment_entry() + pe2.paid_from = self.debtors_usd + pe2.paid_from_account_currency = "USD" + pe2.source_exchange_rate = 75 + pe2.received_amount = 75 * 100 + pe2.save() + # Allocate payment against both invoices + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe2.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) + + # Exc gain/loss JE from PE1 should be available + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 1, + )