From 89d109e8d23a58187f4aaa04a7b015dcfdaaa5f5 Mon Sep 17 00:00:00 2001 From: Gughan Ravikumar Date: Thu, 27 Jul 2023 23:12:11 +0530 Subject: [PATCH 01/55] fix: Naming Series preview when no previous transaction present --- erpnext/accounts/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index e3546631514..9102e0a0bab 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1112,7 +1112,12 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": - date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + if doc: + date = doc.get("posting_date") or doc.get("transaction_date") + company = doc.get("company") + else: + date = getdate() + company = None return get_fiscal_year(date=date, company=doc.get("company"))[0] From cdf100d55260044c1deeaacb2903788015e9395f Mon Sep 17 00:00:00 2001 From: Gughan Ravikumar Date: Tue, 15 Aug 2023 09:38:29 +0530 Subject: [PATCH 02/55] fix: Naming Series preview when no previous transaction present --- erpnext/accounts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9102e0a0bab..80e54cbc58a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1118,7 +1118,7 @@ def parse_naming_series_variable(doc, variable): else: date = getdate() company = None - return get_fiscal_year(date=date, company=doc.get("company"))[0] + return get_fiscal_year(date=date, company=company)[0] @frappe.whitelist() From c47adcfdd9c63b4e99adb775dbb6d1a85cf0c151 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 21 Aug 2023 15:12:11 +0530 Subject: [PATCH 03/55] fix: advance in seperate account --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 ++++++- erpnext/accounts/utils.py | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ac31e8a1dbe..1f5fba52a68 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1123,7 +1123,7 @@ class PaymentEntry(AccountsController): if self.book_advance_payments_in_separate_party_account: gl_entries = [] for d in self.get("references"): - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"): + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"): if not (against_voucher_type and against_voucher) or ( d.reference_doctype == against_voucher_type and d.reference_name == against_voucher ): @@ -1159,6 +1159,10 @@ class PaymentEntry(AccountsController): "voucher_detail_no": invoice.name, } + posting_date = frappe.db.get_value( + invoice.reference_doctype, invoice.reference_name, "posting_date" + ) + dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit" args_dict["account"] = invoice.account args_dict[dr_or_cr] = invoice.allocated_amount @@ -1167,6 +1171,7 @@ class PaymentEntry(AccountsController): { "against_voucher_type": invoice.reference_doctype, "against_voucher": invoice.reference_name, + "posting_date": posting_date, } ) gle = self.get_gl_dict( diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index bccf6f10b63..5c3c486deba 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -489,14 +489,13 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n gl_map = doc.build_gl_map() create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) - if voucher_type == "Payment Entry": - doc.make_advance_gl_entries() - # Only update outstanding for newly linked vouchers for entry in entries: update_voucher_outstanding( entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party ) + if voucher_type == "Payment Entry": + doc.make_advance_gl_entries(entry.against_voucher_type, entry.against_voucher) frappe.flags.ignore_party_validation = False From dc7162329594337ee5b869f42d44fb28d8eaf2e3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 10:26:42 +0530 Subject: [PATCH 04/55] 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 05/55] 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 06/55] 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 07/55] 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 08/55] 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 09/55] 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 10/55] 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 11/55] 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 12/55] 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 13/55] 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 14/55] 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 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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 29/55] 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 e1a94a9ba1c1241012cc0458791686e26ad5483d Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Tue, 5 Sep 2023 21:38:06 +0530 Subject: [PATCH 30/55] fix: move SI and DI connected links to internal_and_external_links --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 4 +++- .../stock/doctype/delivery_note/delivery_note_dashboard.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 6fdcf263a55..fd95c1fe0e5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,9 +15,11 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], - "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, + "internal_and_external_links": { + "Delivery Note": ["items", "delivery_note"], + }, "transactions": [ { "label": _("Payment"), diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index e66c23324da..d4a574da73f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,10 +11,12 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], - "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], }, + "internal_and_external_links": { + "Sales Invoice": ["items", "against_sales_invoice"], + }, "transactions": [ {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, From 1d93d66c30e69bfcb277123462bf822aa3c3c1d4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:59:34 +0530 Subject: [PATCH 31/55] 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 32/55] 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 33/55] 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, + ) From 43061f4416f4c04cbd4442cd128ff504d3ecfc04 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 11 Sep 2023 19:16:25 +0530 Subject: [PATCH 34/55] fix: correct set_query filters --- .../doctype/payment_reconciliation/payment_reconciliation.js | 3 ++- erpnext/buying/doctype/supplier/supplier.js | 1 + erpnext/selling/doctype/customer/customer.js | 1 + erpnext/setup/doctype/company/company.js | 4 ++-- erpnext/setup/doctype/customer_group/customer_group.js | 1 + erpnext/setup/doctype/supplier_group/supplier_group.js | 1 + 6 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 2adc1238b70..9072ce4561e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -24,7 +24,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo filters: { "company": this.frm.doc.company, "is_group": 0, - "account_type": frappe.boot.party_account_types[this.frm.doc.party_type] + "account_type": frappe.boot.party_account_types[this.frm.doc.party_type], + "root_type": this.frm.doc.party_type == 'Customer' ? "Asset" : "Liability" } }; }); diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 372ca56b86b..08dc44c71b4 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -12,6 +12,7 @@ frappe.ui.form.on("Supplier", { return { filters: { 'account_type': 'Payable', + 'root_type': 'Liability', 'company': d.company, "is_group": 0 } diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 60f0941559a..e274a52690b 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -23,6 +23,7 @@ frappe.ui.form.on("Customer", { let d = locals[cdt][cdn]; let filters = { 'account_type': 'Receivable', + 'root_type': 'Asset', 'company': d.company, "is_group": 0 }; diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index fa207ecdd1c..4973dab505e 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -200,8 +200,8 @@ erpnext.company.setup_queries = function(frm) { $.each([ ["default_bank_account", {"account_type": "Bank"}], ["default_cash_account", {"account_type": "Cash"}], - ["default_receivable_account", {"account_type": "Receivable"}], - ["default_payable_account", {"account_type": "Payable"}], + ["default_receivable_account", { "root_type": "Asset", "account_type": "Receivable" }], + ["default_payable_account", { "root_type": "Liability", "account_type": "Payable" }], ["default_expense_account", {"root_type": "Expense"}], ["default_income_account", {"root_type": "Income"}], ["round_off_account", {"root_type": "Expense"}], diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js index 49a90f959d0..3c81b0283ca 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.js +++ b/erpnext/setup/doctype/customer_group/customer_group.js @@ -30,6 +30,7 @@ frappe.ui.form.on("Customer Group", { frm.set_query('account', 'accounts', function (doc, cdt, cdn) { return { filters: { + 'root_type': 'Asset', "account_type": 'Receivable', "company": locals[cdt][cdn].company, "is_group": 0 diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js index b2acfd73559..33629297ffd 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group.js @@ -30,6 +30,7 @@ frappe.ui.form.on("Supplier Group", { frm.set_query('account', 'accounts', function (doc, cdt, cdn) { return { filters: { + 'root_type': 'Liability', 'account_type': 'Payable', 'company': locals[cdt][cdn].company, "is_group": 0 From fae640c56fa21acf18c1ebb7f9aea1a440e7a46e Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 11 Sep 2023 17:57:31 -0300 Subject: [PATCH 35/55] fix: "Based on" field always has the value "Not applicable" --- erpnext/setup/doctype/authorization_rule/authorization_rule.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index 44bd826fc6e..674cd819689 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -55,8 +55,6 @@ class AuthorizationRule(Document): frappe.throw(_("Discount must be less than 100")) elif self.based_on == "Customerwise Discount" and not self.master_name: frappe.throw(_("Customer required for 'Customerwise Discount'")) - else: - self.based_on = "Not Applicable" def validate(self): self.check_duplicate_entry() From 623239d3f717f0af04cd9f8fd1f74607778ad4e3 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 11 Sep 2023 18:14:38 -0300 Subject: [PATCH 36/55] feat: created "based on" Item Group to specify a different percentage for each item group --- .../authorization_control.py | 25 ++++++++++++------- .../authorization_rule/authorization_rule.js | 10 ++++++++ .../authorization_rule.json | 8 +++--- .../authorization_rule/authorization_rule.py | 1 + 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 5e77c6fa816..80d8c9f85a5 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -10,7 +10,7 @@ from erpnext.utilities.transaction_base import TransactionBase class AuthorizationControl(TransactionBase): - def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company): + def get_appr_user_role(self, det, doctype_name, total, based_on, condition, master_name, company): amt_list, appr_users, appr_roles = [], [], [] users, roles = "", "" if det: @@ -47,11 +47,11 @@ class AuthorizationControl(TransactionBase): frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) - def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item=""): + def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""): chk = 1 add_cond1, add_cond2 = "", "" - if based_on == "Itemwise Discount": - add_cond1 += " and master_name = " + frappe.db.escape(cstr(item)) + if based_on in ["Itemwise Discount", "Item Group wise Discount"]: + add_cond1 += " and master_name = " + frappe.db.escape(cstr(master_name)) itemwise_exists = frappe.db.sql( """select value from `tabAuthorization Rule` where transaction = %s and value <= %s @@ -71,11 +71,11 @@ class AuthorizationControl(TransactionBase): if itemwise_exists: self.get_appr_user_role( - itemwise_exists, doctype_name, total, based_on, cond + add_cond1, item, company + itemwise_exists, doctype_name, total, based_on, cond + add_cond1, master_name, company ) chk = 0 if chk == 1: - if based_on == "Itemwise Discount": + if based_on in ["Itemwise Discount", "Item Group wise Discount"]: add_cond2 += " and ifnull(master_name,'') = ''" appr = frappe.db.sql( @@ -95,7 +95,7 @@ class AuthorizationControl(TransactionBase): (doctype_name, total, based_on), ) - self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, item, company) + self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, master_name, company) def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company): add_cond = "" @@ -123,6 +123,12 @@ class AuthorizationControl(TransactionBase): self.validate_auth_rule( doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_code ) + elif based_on == "Item Group wise Discount": + if doc_obj: + for t in doc_obj.get("items"): + self.validate_auth_rule( + doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_group + ) else: self.validate_auth_rule(doctype_name, auth_value, based_on, add_cond, company) @@ -148,6 +154,7 @@ class AuthorizationControl(TransactionBase): "Average Discount", "Customerwise Discount", "Itemwise Discount", + "Item Group wise Discount", ] # Check for authorization set for individual user @@ -166,7 +173,7 @@ class AuthorizationControl(TransactionBase): # Remove user specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != "Itemwise Discount": + if r in final_based_on and not r in ["Itemwise Discount", "Item Group wise Discount"]: final_based_on.remove(r) # Check for authorization set on particular roles @@ -194,7 +201,7 @@ class AuthorizationControl(TransactionBase): # Remove role specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != "Itemwise Discount": + if r in final_based_on and not r in ["Itemwise Discount", "Item Group wise Discount"]: final_based_on.remove(r) # Check for global authorization diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.js b/erpnext/setup/doctype/authorization_rule/authorization_rule.js index 3f6afcae7f5..f00ed3ecd0d 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.js +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.js @@ -12,6 +12,9 @@ frappe.ui.form.on("Authorization Rule", { } else if(frm.doc.based_on==="Itemwise Discount") { unhide_field("master_name"); frm.set_value("customer_or_item", "Item"); + } else if(frm.doc.based_on==="Item Group wise Discount") { + unhide_field("master_name"); + frm.set_value("customer_or_item", "Item Group"); } else { frm.set_value("customer_or_item", ""); frm.set_value("master_name", ""); @@ -81,6 +84,13 @@ cur_frm.fields_dict['master_name'].get_query = function(doc) { doctype: "Item", query: "erpnext.controllers.queries.item_query" } + else if (doc.based_on==="Item Group wise Discount") + return { + doctype: "Item Group", + filters: { + "is_group": 0 + } + } else return { filters: [ diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.json b/erpnext/setup/doctype/authorization_rule/authorization_rule.json index d3b8887c37b..d750c7bb182 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.json +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.json @@ -46,7 +46,7 @@ "label": "Based On", "oldfieldname": "based_on", "oldfieldtype": "Select", - "options": "\nGrand Total\nAverage Discount\nCustomerwise Discount\nItemwise Discount\nNot Applicable", + "options": "\nGrand Total\nAverage Discount\nCustomerwise Discount\nItemwise Discount\nItem Group wise Discount\nNot Applicable", "reqd": 1 }, { @@ -54,14 +54,14 @@ "fieldtype": "Select", "hidden": 1, "label": "Customer or Item", - "options": "Customer\nItem", + "options": "Customer\nItem\nItem Group", "read_only": 1 }, { "fieldname": "master_name", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Customer / Item Name", + "label": "Customer / Item / Item Group", "oldfieldname": "master_name", "oldfieldtype": "Link", "options": "customer_or_item" @@ -162,7 +162,7 @@ "icon": "fa fa-shield", "idx": 1, "links": [], - "modified": "2022-07-01 11:19:45.643991", + "modified": "2023-09-11 10:29:02.863193", "modified_by": "Administrator", "module": "Setup", "name": "Authorization Rule", diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index 44bd826fc6e..b7823a183ea 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -47,6 +47,7 @@ class AuthorizationRule(Document): "Average Discount", "Customerwise Discount", "Itemwise Discount", + "Item Group wise Discount", ]: frappe.throw( _("Cannot set authorization on basis of Discount for {0}").format(self.transaction) From c09807845f4142207da75e1061a8561cdf0d6af6 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 12 Sep 2023 13:25:51 +0530 Subject: [PATCH 37/55] fix: ensure correct preview and set latest transaction date --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1f5fba52a68..3b3c78f3b24 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -98,7 +98,6 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() - self.make_advance_gl_entries() self.update_outstanding_amounts() self.update_advance_paid() self.update_payment_schedule() @@ -152,7 +151,6 @@ class PaymentEntry(AccountsController): ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) - self.make_advance_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() self.delink_advance_entry_references() @@ -1055,6 +1053,8 @@ class PaymentEntry(AccountsController): else: self.make_exchange_gain_loss_journal() + self.make_advance_gl_entries(cancel=cancel) + def add_party_gl_entries(self, gl_entries): if self.party_account: if self.payment_type == "Receive": @@ -1163,6 +1163,9 @@ class PaymentEntry(AccountsController): invoice.reference_doctype, invoice.reference_name, "posting_date" ) + if getdate(posting_date) < getdate(self.posting_date): + posting_date = self.posting_date + dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit" args_dict["account"] = invoice.account args_dict[dr_or_cr] = invoice.allocated_amount From da54ab5b3daa8d2de51277238044061b46c54111 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Tue, 12 Sep 2023 09:43:11 -0300 Subject: [PATCH 38/55] fix: linters --- .../doctype/authorization_control/authorization_control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 80d8c9f85a5..fd5a2012c7d 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -95,7 +95,9 @@ class AuthorizationControl(TransactionBase): (doctype_name, total, based_on), ) - self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, master_name, company) + self.get_appr_user_role( + appr, doctype_name, total, based_on, cond + add_cond2, master_name, company + ) def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company): add_cond = "" From e6c302a397926a9a78b0ef20a03da52e7c48712e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Sep 2023 11:22:43 +0530 Subject: [PATCH 39/55] fix: test case (#37063) --- erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py index d8d9f8fbc69..ee5886c1cb0 100644 --- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -191,7 +191,7 @@ class TestBOMCreator(FrappeTestCase): ) doc = make_bom_creator( - name="Bicycle BOM", + name="Bicycle BOM Test", company="_Test Company", item_code=final_product, qty=1, From 6bab0eeaa1ac53c85a4a7b9668ea4ffadf99be4d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 Sep 2023 15:28:59 +0530 Subject: [PATCH 40/55] fix: Purchase Receipt Provisional Accounting GL Entries --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 60aefddf4c8..04eff54c43f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -603,7 +603,7 @@ class PurchaseReceipt(BuyingController): account=provisional_account, cost_center=item.cost_center, debit=0.0, - credit=multiplication_factor * item.amount, + credit=multiplication_factor * item.base_amount, remarks=remarks, against_account=expense_account, account_currency=credit_currency, @@ -617,7 +617,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=expense_account, cost_center=item.cost_center, - debit=multiplication_factor * item.amount, + debit=multiplication_factor * item.base_amount, credit=0.0, remarks=remarks, against_account=provisional_account, From 328ba4b656021f742a6c1ef961c0376215afaa6f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 9 Sep 2023 19:02:33 +0530 Subject: [PATCH 41/55] refactor!: remove `woocommerce` --- .../connectors/__init__.py | 0 .../connectors/woocommerce_connection.py | 256 ------------------ .../doctype/woocommerce_settings/__init__.py | 0 .../test_woocommerce_settings.py | 8 - .../woocommerce_settings.js | 56 ---- .../woocommerce_settings.json | 175 ------------ .../woocommerce_settings.py | 87 ------ 7 files changed, 582 deletions(-) delete mode 100644 erpnext/erpnext_integrations/connectors/__init__.py delete mode 100644 erpnext/erpnext_integrations/connectors/woocommerce_connection.py delete mode 100644 erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py delete mode 100644 erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js delete mode 100644 erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json delete mode 100644 erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py diff --git a/erpnext/erpnext_integrations/connectors/__init__.py b/erpnext/erpnext_integrations/connectors/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py deleted file mode 100644 index 2b2da7b971b..00000000000 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ /dev/null @@ -1,256 +0,0 @@ -import base64 -import hashlib -import hmac -import json - -import frappe -from frappe import _ -from frappe.utils import cstr - - -def verify_request(): - woocommerce_settings = frappe.get_doc("Woocommerce Settings") - sig = base64.b64encode( - hmac.new( - woocommerce_settings.secret.encode("utf8"), frappe.request.data, hashlib.sha256 - ).digest() - ) - - if ( - frappe.request.data - and not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode() - ): - frappe.throw(_("Unverified Webhook Data")) - frappe.set_user(woocommerce_settings.creation_user) - - -@frappe.whitelist(allow_guest=True) -def order(*args, **kwargs): - try: - _order(*args, **kwargs) - except Exception: - error_message = ( - frappe.get_traceback() + "\n\n Request Data: \n" + json.loads(frappe.request.data).__str__() - ) - frappe.log_error("WooCommerce Error", error_message) - raise - - -def _order(*args, **kwargs): - woocommerce_settings = frappe.get_doc("Woocommerce Settings") - if frappe.flags.woocomm_test_order_data: - order = frappe.flags.woocomm_test_order_data - event = "created" - # Ignore the test ping issued during WooCommerce webhook configuration - # Ref: https://github.com/woocommerce/woocommerce/issues/15642 - if frappe.request.data.decode("utf-8").startswith("webhook_id="): - return "success" - elif frappe.request and frappe.request.data: - verify_request() - try: - order = json.loads(frappe.request.data) - except ValueError: - # woocommerce returns 'webhook_id=value' for the first request which is not JSON - order = frappe.request.data - event = frappe.get_request_header("X-Wc-Webhook-Event") - - else: - return "success" - - if event == "created": - sys_lang = frappe.get_single("System Settings").language or "en" - raw_billing_data = order.get("billing") - raw_shipping_data = order.get("shipping") - customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") - link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name) - link_items(order.get("line_items"), woocommerce_settings, sys_lang) - create_sales_order(order, woocommerce_settings, customer_name, sys_lang) - - -def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name): - customer_woo_com_email = raw_billing_data.get("email") - customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email}) - if not customer_exists: - # Create Customer - customer = frappe.new_doc("Customer") - else: - # Edit Customer - customer = frappe.get_doc("Customer", {"woocommerce_email": customer_woo_com_email}) - old_name = customer.customer_name - - customer.customer_name = customer_name - customer.woocommerce_email = customer_woo_com_email - customer.flags.ignore_mandatory = True - customer.save() - - if customer_exists: - # Fixes https://github.com/frappe/erpnext/issues/33708 - if old_name != customer_name: - frappe.rename_doc("Customer", old_name, customer_name) - for address_type in ( - "Billing", - "Shipping", - ): - try: - address = frappe.get_doc( - "Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type} - ) - rename_address(address, customer) - except ( - frappe.DoesNotExistError, - frappe.DuplicateEntryError, - frappe.ValidationError, - ): - pass - else: - create_address(raw_billing_data, customer, "Billing") - create_address(raw_shipping_data, customer, "Shipping") - create_contact(raw_billing_data, customer) - - -def create_contact(data, customer): - email = data.get("email", None) - phone = data.get("phone", None) - - if not email and not phone: - return - - contact = frappe.new_doc("Contact") - contact.first_name = data.get("first_name") - contact.last_name = data.get("last_name") - contact.is_primary_contact = 1 - contact.is_billing_contact = 1 - - if phone: - contact.add_phone(phone, is_primary_mobile_no=1, is_primary_phone=1) - - if email: - contact.add_email(email, is_primary=1) - - contact.append("links", {"link_doctype": "Customer", "link_name": customer.name}) - - contact.flags.ignore_mandatory = True - contact.save() - - -def create_address(raw_data, customer, address_type): - address = frappe.new_doc("Address") - - address.address_line1 = raw_data.get("address_1", "Not Provided") - address.address_line2 = raw_data.get("address_2", "Not Provided") - address.city = raw_data.get("city", "Not Provided") - address.woocommerce_email = customer.woocommerce_email - address.address_type = address_type - address.country = frappe.get_value("Country", {"code": raw_data.get("country", "IN").lower()}) - address.state = raw_data.get("state") - address.pincode = raw_data.get("postcode") - address.phone = raw_data.get("phone") - address.email_id = customer.woocommerce_email - address.append("links", {"link_doctype": "Customer", "link_name": customer.name}) - - address.flags.ignore_mandatory = True - address.save() - - -def rename_address(address, customer): - old_address_title = address.name - new_address_title = customer.name + "-" + address.address_type - address.address_title = customer.customer_name - address.save() - - frappe.rename_doc("Address", old_address_title, new_address_title) - - -def link_items(items_list, woocommerce_settings, sys_lang): - for item_data in items_list: - item_woo_com_id = cstr(item_data.get("product_id")) - - if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, "name"): - # Create Item - item = frappe.new_doc("Item") - item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id) - item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang) - item.item_group = _("WooCommerce Products", sys_lang) - - item.item_name = item_data.get("name") - item.woocommerce_id = item_woo_com_id - item.flags.ignore_mandatory = True - item.save() - - -def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): - new_sales_order = frappe.new_doc("Sales Order") - new_sales_order.customer = customer_name - - new_sales_order.po_no = new_sales_order.woocommerce_id = order.get("id") - new_sales_order.naming_series = woocommerce_settings.sales_order_series or "SO-WOO-" - - created_date = order.get("date_created").split("T") - new_sales_order.transaction_date = created_date[0] - delivery_after = woocommerce_settings.delivery_after_days or 7 - new_sales_order.delivery_date = frappe.utils.add_days(created_date[0], delivery_after) - - new_sales_order.company = woocommerce_settings.company - - set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang) - new_sales_order.flags.ignore_mandatory = True - new_sales_order.insert() - new_sales_order.submit() - - frappe.db.commit() - - -def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang): - company_abbr = frappe.db.get_value("Company", woocommerce_settings.company, "abbr") - - default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr) - if not frappe.db.exists("Warehouse", default_warehouse) and not woocommerce_settings.warehouse: - frappe.throw(_("Please set Warehouse in Woocommerce Settings")) - - for item in order.get("line_items"): - woocomm_item_id = item.get("product_id") - found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)}) - - ordered_items_tax = item.get("total_tax") - - new_sales_order.append( - "items", - { - "item_code": found_item.name, - "item_name": found_item.item_name, - "description": found_item.item_name, - "delivery_date": new_sales_order.delivery_date, - "uom": woocommerce_settings.uom or _("Nos", sys_lang), - "qty": item.get("quantity"), - "rate": item.get("price"), - "warehouse": woocommerce_settings.warehouse or default_warehouse, - }, - ) - - add_tax_details( - new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account - ) - - # shipping_details = order.get("shipping_lines") # used for detailed order - - add_tax_details( - new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account - ) - add_tax_details( - new_sales_order, - order.get("shipping_total"), - "Shipping Total", - woocommerce_settings.f_n_f_account, - ) - - -def add_tax_details(sales_order, price, desc, tax_account_head): - sales_order.append( - "taxes", - { - "charge_type": "Actual", - "account_head": tax_account_head, - "tax_amount": price, - "description": desc, - }, - ) diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py deleted file mode 100644 index 9945823bf73..00000000000 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestWoocommerceSettings(unittest.TestCase): - pass diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js deleted file mode 100644 index d7a3d36a5f1..00000000000 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Woocommerce Settings', { - refresh (frm) { - frm.trigger("add_button_generate_secret"); - frm.trigger("check_enabled"); - frm.set_query("tax_account", ()=>{ - return { - "filters": { - "company": frappe.defaults.get_default("company"), - "is_group": 0 - } - }; - }); - }, - - enable_sync (frm) { - frm.trigger("check_enabled"); - }, - - add_button_generate_secret(frm) { - frm.add_custom_button(__('Generate Secret'), () => { - frappe.confirm( - __("Apps using current key won't be able to access, are you sure?"), - () => { - frappe.call({ - type:"POST", - method:"erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret", - }).done(() => { - frm.reload_doc(); - }).fail(() => { - frappe.msgprint(__("Could not generate Secret")); - }); - } - ); - }); - }, - - check_enabled (frm) { - frm.set_df_property("woocommerce_server_url", "reqd", frm.doc.enable_sync); - frm.set_df_property("api_consumer_key", "reqd", frm.doc.enable_sync); - frm.set_df_property("api_consumer_secret", "reqd", frm.doc.enable_sync); - } -}); - -frappe.ui.form.on("Woocommerce Settings", "onload", function () { - frappe.call({ - method: "erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.get_series", - callback: function (r) { - $.each(r.message, function (key, value) { - set_field_options(key, value); - }); - } - }); -}); diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json deleted file mode 100644 index 956ae09cbd6..00000000000 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "creation": "2018-02-12 15:10:05.495713", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enable_sync", - "sb_00", - "woocommerce_server_url", - "secret", - "cb_00", - "api_consumer_key", - "api_consumer_secret", - "sb_accounting_details", - "tax_account", - "column_break_10", - "f_n_f_account", - "defaults_section", - "creation_user", - "warehouse", - "sales_order_series", - "column_break_14", - "company", - "delivery_after_days", - "uom", - "endpoints", - "endpoint" - ], - "fields": [ - { - "default": "0", - "fieldname": "enable_sync", - "fieldtype": "Check", - "label": "Enable Sync" - }, - { - "fieldname": "sb_00", - "fieldtype": "Section Break" - }, - { - "fieldname": "woocommerce_server_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Woocommerce Server URL" - }, - { - "fieldname": "secret", - "fieldtype": "Code", - "label": "Secret", - "read_only": 1 - }, - { - "fieldname": "cb_00", - "fieldtype": "Column Break" - }, - { - "fieldname": "api_consumer_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "API consumer key" - }, - { - "fieldname": "api_consumer_secret", - "fieldtype": "Data", - "in_list_view": 1, - "label": "API consumer secret" - }, - { - "fieldname": "sb_accounting_details", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "tax_account", - "fieldtype": "Link", - "label": "Tax Account", - "options": "Account", - "reqd": 1 - }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { - "fieldname": "f_n_f_account", - "fieldtype": "Link", - "label": "Freight and Forwarding Account", - "options": "Account", - "reqd": 1 - }, - { - "fieldname": "defaults_section", - "fieldtype": "Section Break", - "label": "Defaults" - }, - { - "description": "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.", - "fieldname": "creation_user", - "fieldtype": "Link", - "label": "Creation User", - "options": "User", - "reqd": 1 - }, - { - "description": "This warehouse will be used to create Sales Orders. The fallback warehouse is \"Stores\".", - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "column_break_14", - "fieldtype": "Column Break" - }, - { - "description": "The fallback series is \"SO-WOO-\".", - "fieldname": "sales_order_series", - "fieldtype": "Select", - "label": "Sales Order Series" - }, - { - "description": "This is the default UOM used for items and Sales orders. The fallback UOM is \"Nos\".", - "fieldname": "uom", - "fieldtype": "Link", - "label": "UOM", - "options": "UOM" - }, - { - "fieldname": "endpoints", - "fieldtype": "Section Break", - "label": "Endpoints" - }, - { - "fieldname": "endpoint", - "fieldtype": "Code", - "label": "Endpoint", - "read_only": 1 - }, - { - "description": "This company will be used to create Sales Orders.", - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "description": "This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.", - "fieldname": "delivery_after_days", - "fieldtype": "Int", - "label": "Delivery After (Days)" - } - ], - "issingle": 1, - "modified": "2019-11-04 00:45:21.232096", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Woocommerce Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py deleted file mode 100644 index 4aa98aab56b..00000000000 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from urllib.parse import urlparse - -import frappe -from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.model.document import Document -from frappe.utils.nestedset import get_root_of - - -class WoocommerceSettings(Document): - def validate(self): - self.validate_settings() - self.create_delete_custom_fields() - self.create_webhook_url() - - def create_delete_custom_fields(self): - if self.enable_sync: - create_custom_fields( - { - ("Customer", "Sales Order", "Item", "Address"): dict( - fieldname="woocommerce_id", - label="Woocommerce ID", - fieldtype="Data", - read_only=1, - print_hide=1, - ), - ("Customer", "Address"): dict( - fieldname="woocommerce_email", - label="Woocommerce Email", - fieldtype="Data", - read_only=1, - print_hide=1, - ), - } - ) - - if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}): - item_group = frappe.new_doc("Item Group") - item_group.item_group_name = _("WooCommerce Products") - item_group.parent_item_group = get_root_of("Item Group") - item_group.insert() - - def validate_settings(self): - if self.enable_sync: - if not self.secret: - self.set("secret", frappe.generate_hash()) - - if not self.woocommerce_server_url: - frappe.throw(_("Please enter Woocommerce Server URL")) - - if not self.api_consumer_key: - frappe.throw(_("Please enter API Consumer Key")) - - if not self.api_consumer_secret: - frappe.throw(_("Please enter API Consumer Secret")) - - def create_webhook_url(self): - endpoint = "/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order" - - try: - url = frappe.request.url - except RuntimeError: - # for CI Test to work - url = "http://localhost:8000" - - server_url = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url)) - - delivery_url = server_url + endpoint - self.endpoint = delivery_url - - -@frappe.whitelist() -def generate_secret(): - woocommerce_settings = frappe.get_doc("Woocommerce Settings") - woocommerce_settings.secret = frappe.generate_hash() - woocommerce_settings.save() - - -@frappe.whitelist() -def get_series(): - return { - "sales_order_series": frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-", - } From 74272a2e28fddfd292e79a72dd050c4352eefa86 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 Sep 2023 12:02:27 +0530 Subject: [PATCH 42/55] fix: patch to delete `Woocommerce Settings` DocType --- erpnext/patches.txt | 1 + erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b3d6d3e80a4..d0ee2e4dc4e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -340,5 +340,6 @@ erpnext.patches.v15_0.remove_exotel_integration erpnext.patches.v14_0.single_to_multi_dunning execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow +erpnext.patches.v15_0.delete_woocommerce_settings_doctype # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py new file mode 100644 index 00000000000..fb92ca55d17 --- /dev/null +++ b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py @@ -0,0 +1,5 @@ +import frappe + + +def execute(): + frappe.delete_doc("DocType", "Woocommerce Settings", ignore_missing=True) From 1c78a5a9aa2dbd4d77e7f411699c4cb7dd265cc9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 Sep 2023 23:25:02 +0530 Subject: [PATCH 43/55] test: Purchase Receipt Provisional Accounting GL Entries --- .../buying/doctype/supplier/test_supplier.py | 1 + .../purchase_receipt/test_purchase_receipt.py | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 7be1d834a65..ee2ada3b655 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -206,6 +206,7 @@ def create_supplier(**args): { "doctype": "Supplier", "supplier_name": args.supplier_name, + "default_currency": args.default_currency, "supplier_group": args.supplier_group or "Services", "supplier_type": args.supplier_type or "Company", "tax_withholding_category": args.tax_withholding_category, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6134bfa1f23..b7712ee5ce2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2017,6 +2017,49 @@ class TestPurchaseReceipt(FrappeTestCase): ste7.reload() self.assertEqual(ste7.items[0].valuation_rate, valuation_rate) + def test_purchase_receipt_provisional_accounting(self): + # Step - 1: Create Supplier with Default Currency as USD + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + supplier = create_supplier(default_currency="USD") + + # Step - 2: Setup Company for Provisional Accounting + from erpnext.accounts.doctype.account.test_account import create_account + + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + company = frappe.get_doc("Company", "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + # Step - 3: Create Non-Stock Item + item = make_item(properties={"is_stock_item": 0}) + + # Step - 4: Create Purchase Receipt + pr = make_purchase_receipt( + qty=2, + item_code=item.name, + company=company.name, + supplier=supplier.name, + currency=supplier.default_currency, + ) + + # Test - 1: Total and Base Total should not be the same as the currency is different + self.assertNotEqual(flt(pr.total, 2), flt(pr.base_total, 2)) + self.assertEqual(flt(pr.total * pr.conversion_rate, 2), flt(pr.base_total, 2)) + + # Test - 2: Sum of Debit or Credit should be equal to Purchase Receipt Base Total + amount = frappe.db.get_value("GL Entry", {"docstatus": 1, "voucher_no": pr.name}, ["sum(debit)"]) + expected_amount = pr.base_total + self.assertEqual(amount, expected_amount) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 0e83190c190ea122fd6d221704c29556b0394d48 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Sep 2023 13:34:40 +0530 Subject: [PATCH 44/55] fix: incorrect stock ledger entries in DN (#36944) --- erpnext/stock/doctype/delivery_note/delivery_note.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 6a9e2414445..e0d49192eb1 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1253,6 +1253,7 @@ "depends_on": "eval: doc.is_internal_customer", "fieldname": "set_target_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Set Target Warehouse", "no_copy": 1, @@ -1400,7 +1401,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-06-16 14:58:55.066602", + "modified": "2023-09-04 14:15:28.363184", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From d2f32861158236964ba18daa01144ea455607d80 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 Sep 2023 16:26:29 +0530 Subject: [PATCH 45/55] fix: ignore user permissions for `From Warehouse` in PR --- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++- .../doctype/purchase_order_item/purchase_order_item.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index b242108a9a9..5b5cc2b0217 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1173,6 +1173,7 @@ "depends_on": "is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1273,7 +1274,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:19:45.710444", + "modified": "2023-09-13 16:21:07.361700", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 414f0866ccb..f79b6223bf5 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -878,6 +878,7 @@ "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "From Warehouse", "options": "Warehouse" }, @@ -902,7 +903,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-17 10:17:40.893393", + "modified": "2023-09-13 16:22:40.825092", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 96363dbb07ebb7f22e6450a2e4600d4b80deb4c3 Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:34:20 +0530 Subject: [PATCH 46/55] fix: Remove redundant code (#37001) fix: Remove redundant code --- erpnext/e_commerce/shopping_cart/cart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 85d9a6585ce..c66ae1d6009 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -631,7 +631,6 @@ def get_applicable_shipping_rules(party=None, quotation=None): shipping_rules = get_shipping_rules(quotation) if shipping_rules: - rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label") # we need this in sorted order as per the position of the rule in the settings page return [[rule, rule] for rule in shipping_rules] From 6f4f5be9904e482cbb21a3aa4b5d3555c6d4eeeb Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 13 Sep 2023 18:08:45 +0300 Subject: [PATCH 47/55] chore: Translate Voucher Type in General Ledger Report (#36874) chore: Translate Voucher Type in General Ledger Report --- erpnext/accounts/report/general_ledger/general_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ae24675ff57..79bfd7833ab 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -434,6 +434,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) + gle.voucher_type = _(gle.voucher_type) if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: From 2135b0132dd4969b2b48e170ab499a0d15b27039 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 12 Sep 2023 15:58:10 +0530 Subject: [PATCH 48/55] refactor!: remove twitter integration --- .../social_media_post/social_media_post.js | 16 +- .../social_media_post/social_media_post.json | 45 +----- .../social_media_post/social_media_post.py | 14 +- .../crm/doctype/twitter_settings/__init__.py | 0 .../twitter_settings/test_twitter_settings.py | 9 -- .../twitter_settings/twitter_settings.js | 59 -------- .../twitter_settings/twitter_settings.json | 102 ------------- .../twitter_settings/twitter_settings.py | 141 ------------------ erpnext/patches.txt | 1 + 9 files changed, 6 insertions(+), 381 deletions(-) delete mode 100644 erpnext/crm/doctype/twitter_settings/__init__.py delete mode 100644 erpnext/crm/doctype/twitter_settings/test_twitter_settings.py delete mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.js delete mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.json delete mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.py diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index d4ac0bad16c..71d044c38d8 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { validate: function(frm) { - if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) { + if (frm.doc.linkedin === 0) { frappe.throw(__("Select atleast one Social Media Platform to Share on.")); } if (frm.doc.scheduled_time) { @@ -45,13 +45,6 @@ frappe.ui.form.on('Social Media Post', { } let datasets = [], colors = []; - if (r.message && r.message.twitter) { - colors.push('#1DA1F2'); - datasets.push({ - name: 'Twitter', - values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count] - }); - } if (r.message && r.message.linkedin) { colors.push('#0077b5'); datasets.push({ @@ -104,13 +97,6 @@ frappe.ui.form.on('Social Media Post', { if (frm.doc.post_status !='Deleted') { let html=''; - if (frm.doc.twitter) { - let color = frm.doc.twitter_post_id ? "green" : "red"; - let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += `
- Twitter : ${status} -
` ; - } if (frm.doc.linkedin) { let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 98e78f949e8..1e3e01c36fe 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -12,14 +12,8 @@ "scheduled_time", "post_status", "column_break_6", - "twitter", "linkedin", - "twitter_post_id", "linkedin_post_id", - "content", - "text", - "column_break_14", - "tweet_preview", "linkedin_section", "linkedin_post", "column_break_15", @@ -28,23 +22,11 @@ "amended_from" ], "fields": [ - { - "fieldname": "text", - "fieldtype": "Small Text", - "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1" - }, { "fieldname": "image", "fieldtype": "Attach Image", "label": "Image" }, - { - "default": "1", - "fieldname": "twitter", - "fieldtype": "Check", - "label": "Twitter" - }, { "default": "1", "fieldname": "linkedin", @@ -60,12 +42,6 @@ "print_hide": 1, "read_only": 1 }, - { - "depends_on": "eval:doc.twitter ==1", - "fieldname": "content", - "fieldtype": "Section Break", - "label": "Twitter" - }, { "allow_on_submit": 1, "fieldname": "post_status", @@ -75,15 +51,6 @@ "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError", "read_only": 1 }, - { - "allow_on_submit": 1, - "fieldname": "twitter_post_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Twitter Post Id", - "no_copy": 1, - "read_only": 1 - }, { "allow_on_submit": 1, "fieldname": "linkedin_post_id", @@ -105,14 +72,6 @@ "fieldtype": "Column Break", "label": "Share On" }, - { - "fieldname": "column_break_14", - "fieldtype": "Column Break" - }, - { - "fieldname": "tweet_preview", - "fieldtype": "HTML" - }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", @@ -152,10 +111,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-14 14:24:59.821223", + "modified": "2023-09-14 11:24:29.105683", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -203,6 +163,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 3654d29bdc0..9615a8389ca 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -11,7 +11,7 @@ from frappe.model.document import Document class SocialMediaPost(Document): def validate(self): - if not self.twitter and not self.linkedin: + if not self.linkedin: frappe.throw(_("Select atleast one Social Media Platform to Share on.")) if self.scheduled_time: @@ -33,10 +33,6 @@ class SocialMediaPost(Document): @frappe.whitelist() def delete_post(self): - if self.twitter and self.twitter_post_id: - twitter = frappe.get_doc("Twitter Settings") - twitter.delete_tweet(self.twitter_post_id) - if self.linkedin and self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") linkedin.delete_post(self.linkedin_post_id) @@ -49,19 +45,11 @@ class SocialMediaPost(Document): if self.linkedin and self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") response["linkedin"] = linkedin.get_post(self.linkedin_post_id) - if self.twitter and self.twitter_post_id: - twitter = frappe.get_doc("Twitter Settings") - response["twitter"] = twitter.get_tweet(self.twitter_post_id) - return response @frappe.whitelist() def post(self): try: - if self.twitter and not self.twitter_post_id: - twitter = frappe.get_doc("Twitter Settings") - twitter_post = twitter.post(self.text, self.image) - self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image) diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py deleted file mode 100644 index 9dbce8f8aba..00000000000 --- a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestTwitterSettings(unittest.TestCase): - pass diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js deleted file mode 100644 index c322092d6f3..00000000000 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Twitter Settings', { - onload: function(frm) { - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ - frappe.confirm( - __('Session not valid, Do you want to login?'), - function(){ - frm.trigger("login"); - }, - function(){ - window.close(); - } - ); - } - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`])); - }, - refresh: function(frm) { - let msg, color, flag=false; - if (frm.doc.session_status == "Active") { - msg = __("Session Active"); - color = 'green'; - flag = true; - } - else if(frm.doc.consumer_key && frm.doc.consumer_secret) { - msg = __("Session Not Active. Save doc to login."); - color = 'red'; - flag = true; - } - - if (flag) { - frm.dashboard.set_headline_alert( - `
-
- -
-
` - ); - } - }, - login: function(frm) { - if (frm.doc.consumer_key && frm.doc.consumer_secret){ - frappe.dom.freeze(); - frappe.call({ - doc: frm.doc, - method: "get_authorize_url", - callback : function(r) { - window.location.href = r.message; - } - }).fail(function() { - frappe.dom.unfreeze(); - }); - } - }, - after_save: function(frm) { - frm.trigger("login"); - } -}); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json deleted file mode 100644 index 8d05877f060..00000000000 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "actions": [], - "creation": "2020-01-30 10:29:08.562108", - "doctype": "DocType", - "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "account_name", - "profile_pic", - "oauth_details", - "consumer_key", - "column_break_5", - "consumer_secret", - "access_token", - "access_token_secret", - "session_status" - ], - "fields": [ - { - "fieldname": "account_name", - "fieldtype": "Data", - "label": "Account Name", - "read_only": 1 - }, - { - "fieldname": "oauth_details", - "fieldtype": "Section Break", - "label": "OAuth Credentials" - }, - { - "fieldname": "consumer_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "API Key", - "reqd": 1 - }, - { - "fieldname": "consumer_secret", - "fieldtype": "Password", - "in_list_view": 1, - "label": "API Secret Key", - "reqd": 1 - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "profile_pic", - "fieldtype": "Attach Image", - "hidden": 1, - "read_only": 1 - }, - { - "fieldname": "session_status", - "fieldtype": "Select", - "hidden": 1, - "label": "Session Status", - "options": "Expired\nActive", - "read_only": 1 - }, - { - "fieldname": "access_token", - "fieldtype": "Data", - "hidden": 1, - "label": "Access Token", - "read_only": 1 - }, - { - "fieldname": "access_token_secret", - "fieldtype": "Data", - "hidden": 1, - "label": "Access Token Secret", - "read_only": 1 - } - ], - "image_field": "profile_pic", - "issingle": 1, - "links": [], - "modified": "2021-02-18 15:18:07.900031", - "modified_by": "Administrator", - "module": "CRM", - "name": "Twitter Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py deleted file mode 100644 index 442aa77a5ff..00000000000 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json - -import frappe -import tweepy -from frappe import _ -from frappe.model.document import Document -from frappe.utils import get_url_to_form -from frappe.utils.file_manager import get_file_path - - -class TwitterSettings(Document): - @frappe.whitelist() - def get_authorize_url(self): - callback_url = ( - "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format( - frappe.utils.get_url() - ) - ) - auth = tweepy.OAuth1UserHandler( - self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url - ) - try: - redirect_url = auth.get_authorization_url() - return redirect_url - except (tweepy.TweepyException, tweepy.HTTPException) as e: - frappe.msgprint(_("Error! Failed to get request token.")) - frappe.throw( - _("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")) - ) - - def get_access_token(self, oauth_token, oauth_verifier): - auth = tweepy.OAuth1UserHandler( - self.consumer_key, self.get_password(fieldname="consumer_secret") - ) - auth.request_token = {"oauth_token": oauth_token, "oauth_token_secret": oauth_verifier} - - try: - auth.get_access_token(oauth_verifier) - self.access_token = auth.access_token - self.access_token_secret = auth.access_token_secret - api = self.get_api() - user = api.me() - profile_pic = (user._json["profile_image_url"]).replace("_normal", "") - - frappe.db.set_value( - self.doctype, - self.name, - { - "access_token": auth.access_token, - "access_token_secret": auth.access_token_secret, - "account_name": user._json["screen_name"], - "profile_pic": profile_pic, - "session_status": "Active", - }, - ) - - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings") - except (tweepy.TweepyException, tweepy.HTTPException) as e: - frappe.msgprint(_("Error! Failed to get access token.")) - frappe.throw(_("Invalid Consumer Key or Consumer Secret Key")) - - def get_api(self): - # authentication of consumer key and secret - auth = tweepy.OAuth1UserHandler( - self.consumer_key, self.get_password(fieldname="consumer_secret") - ) - # authentication of access token and secret - auth.set_access_token(self.access_token, self.access_token_secret) - - return tweepy.API(auth) - - def post(self, text, media=None): - if not media: - return self.send_tweet(text) - - if media: - media_id = self.upload_image(media) - return self.send_tweet(text, media_id) - - def upload_image(self, media): - media = get_file_path(media) - api = self.get_api() - media = api.media_upload(media) - - return media.media_id - - def send_tweet(self, text, media_id=None): - api = self.get_api() - try: - if media_id: - response = api.update_status(status=text, media_ids=[media_id]) - else: - response = api.update_status(status=text) - - return response - - except (tweepy.TweepyException, tweepy.HTTPException) as e: - self.api_error(e) - - def delete_tweet(self, tweet_id): - api = self.get_api() - try: - api.destroy_status(tweet_id) - except (tweepy.TweepyException, tweepy.HTTPException) as e: - self.api_error(e) - - def get_tweet(self, tweet_id): - api = self.get_api() - try: - response = api.get_status(tweet_id, trim_user=True, include_entities=True) - except (tweepy.TweepyException, tweepy.HTTPException) as e: - self.api_error(e) - - return response._json - - def api_error(self, e): - content = json.loads(e.response.content) - content = content["errors"][0] - if e.response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw( - content["message"], - title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason), - ) - - -@frappe.whitelist(allow_guest=True) -def callback(oauth_token=None, oauth_verifier=None): - if oauth_token and oauth_verifier: - twitter_settings = frappe.get_single("Twitter Settings") - twitter_settings.get_access_token(oauth_token, oauth_verifier) - frappe.db.commit() - else: - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings") diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d0ee2e4dc4e..22c37159cdd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,6 +339,7 @@ execute:frappe.defaults.clear_default("fiscal_year") erpnext.patches.v15_0.remove_exotel_integration erpnext.patches.v14_0.single_to_multi_dunning execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) +execute:frappe.delete_doc('DocType', 'Twitter Settings', ignore_missing=True) erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow erpnext.patches.v15_0.delete_woocommerce_settings_doctype # below migration patch should always run last From 5976d0d13ff5d57594ba334cc4dac90059448278 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:05:05 +0530 Subject: [PATCH 49/55] fix: prorate factor in subscription invoice total (#36880) fix: prorate factor calculation --- .../subscription_plan/subscription_plan.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index f3acdc5aa87..75223c2ccca 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -57,18 +57,17 @@ def get_plan_rate( prorate = frappe.db.get_single_value("Subscription Settings", "prorate") if prorate: - prorate_factor = flt( - date_diff(start_date, get_first_day(start_date)) - / date_diff(get_last_day(start_date), get_first_day(start_date)), - 1, - ) - - prorate_factor += flt( - date_diff(get_last_day(end_date), end_date) - / date_diff(get_last_day(end_date), get_first_day(end_date)), - 1, - ) - - cost -= plan.cost * prorate_factor - + cost -= plan.cost * get_prorate_factor(start_date, end_date) return cost + + +def get_prorate_factor(start_date, end_date): + total_days_to_skip = date_diff(start_date, get_first_day(start_date)) + total_days_in_month = int(get_last_day(start_date).strftime("%d")) + prorate_factor = flt(total_days_to_skip / total_days_in_month) + + total_days_to_skip = date_diff(get_last_day(end_date), end_date) + total_days_in_month = int(get_last_day(end_date).strftime("%d")) + prorate_factor += flt(total_days_to_skip / total_days_in_month) + + return prorate_factor From 2dbdc402bb13915ba5b7c025951168b92615006a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 14 Sep 2023 12:04:52 +0530 Subject: [PATCH 50/55] refactor!: remove social media post module --- .../crm/doctype/linkedin_settings/__init__.py | 0 .../linkedin_settings/linkedin_settings.js | 74 ------ .../linkedin_settings/linkedin_settings.json | 112 --------- .../linkedin_settings/linkedin_settings.py | 208 ----------------- .../test_linkedin_settings.py | 9 - .../crm/doctype/social_media_post/__init__.py | 0 .../social_media_post/social_media_post.js | 125 ---------- .../social_media_post/social_media_post.json | 169 -------------- .../social_media_post/social_media_post.py | 77 ------ .../social_media_post_list.js | 11 - .../test_social_media_post.py | 9 - erpnext/crm/workspace/crm/crm.json | 221 ++++++++---------- erpnext/hooks.py | 3 - erpnext/patches.txt | 2 + 14 files changed, 96 insertions(+), 924 deletions(-) delete mode 100644 erpnext/crm/doctype/linkedin_settings/__init__.py delete mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.js delete mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.json delete mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.py delete mode 100644 erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py delete mode 100644 erpnext/crm/doctype/social_media_post/__init__.py delete mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.js delete mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.json delete mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.py delete mode 100644 erpnext/crm/doctype/social_media_post/social_media_post_list.js delete mode 100644 erpnext/crm/doctype/social_media_post/test_social_media_post.py diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js deleted file mode 100644 index 7d6b3955cde..00000000000 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('LinkedIn Settings', { - onload: function(frm) { - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) { - frappe.confirm( - __('Session not valid. Do you want to login?'), - function(){ - frm.trigger("login"); - }, - function(){ - window.close(); - } - ); - } - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`])); - }, - refresh: function(frm) { - if (frm.doc.session_status=="Expired"){ - let msg = __("Session not active. Save document to login."); - frm.dashboard.set_headline_alert( - `
-
- -
-
` - ); - } - - if (frm.doc.session_status=="Active"){ - let d = new Date(frm.doc.modified); - d.setDate(d.getDate()+60); - let dn = new Date(); - let days = d.getTime() - dn.getTime(); - days = Math.floor(days/(1000 * 3600 * 24)); - let msg,color; - - if (days>0){ - msg = __("Your session will be expire in {0} days.", [days]); - color = "green"; - } - else { - msg = __("Session is expired. Save doc to login."); - color = "red"; - } - - frm.dashboard.set_headline_alert( - `
-
- -
-
` - ); - } - }, - login: function(frm) { - if (frm.doc.consumer_key && frm.doc.consumer_secret){ - frappe.dom.freeze(); - frappe.call({ - doc: frm.doc, - method: "get_authorization_url", - callback : function(r) { - window.location.href = r.message; - } - }).fail(function() { - frappe.dom.unfreeze(); - }); - } - }, - after_save: function(frm) { - frm.trigger("login"); - } -}); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json deleted file mode 100644 index f882e36c32a..00000000000 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "actions": [], - "creation": "2020-01-30 13:36:39.492931", - "doctype": "DocType", - "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "account_name", - "column_break_2", - "company_id", - "oauth_details", - "consumer_key", - "column_break_5", - "consumer_secret", - "user_details_section", - "access_token", - "person_urn", - "session_status" - ], - "fields": [ - { - "fieldname": "account_name", - "fieldtype": "Data", - "label": "Account Name", - "read_only": 1 - }, - { - "fieldname": "oauth_details", - "fieldtype": "Section Break", - "label": "OAuth Credentials" - }, - { - "fieldname": "consumer_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Consumer Key", - "reqd": 1 - }, - { - "fieldname": "consumer_secret", - "fieldtype": "Password", - "in_list_view": 1, - "label": "Consumer Secret", - "reqd": 1 - }, - { - "fieldname": "access_token", - "fieldtype": "Data", - "hidden": 1, - "label": "Access Token", - "read_only": 1 - }, - { - "fieldname": "person_urn", - "fieldtype": "Data", - "hidden": 1, - "label": "Person URN", - "read_only": 1 - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "user_details_section", - "fieldtype": "Section Break", - "label": "User Details" - }, - { - "fieldname": "session_status", - "fieldtype": "Select", - "hidden": 1, - "label": "Session Status", - "options": "Expired\nActive", - "read_only": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "company_id", - "fieldtype": "Data", - "label": "Company ID", - "reqd": 1 - } - ], - "issingle": 1, - "links": [], - "modified": "2021-02-18 15:19:21.920725", - "modified_by": "Administrator", - "module": "CRM", - "name": "LinkedIn Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py deleted file mode 100644 index 64b3a017b46..00000000000 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from urllib.parse import urlencode - -import frappe -import requests -from frappe import _ -from frappe.model.document import Document -from frappe.utils import get_url_to_form -from frappe.utils.file_manager import get_file_path - - -class LinkedInSettings(Document): - @frappe.whitelist() - def get_authorization_url(self): - params = urlencode( - { - "response_type": "code", - "client_id": self.consumer_key, - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( - frappe.utils.get_url() - ), - "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social", - } - ) - - url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) - - return url - - def get_access_token(self, code): - url = "https://www.linkedin.com/oauth/v2/accessToken" - body = { - "grant_type": "authorization_code", - "code": code, - "client_id": self.consumer_key, - "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( - frappe.utils.get_url() - ), - } - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - response = self.http_post(url=url, data=body, headers=headers) - response = frappe.parse_json(response.content.decode()) - self.db_set("access_token", response["access_token"]) - - def get_member_profile(self): - response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers()) - response = frappe.parse_json(response.content.decode()) - - frappe.db.set_value( - self.doctype, - self.name, - { - "person_urn": response["id"], - "account_name": response["vanityName"], - "session_status": "Active", - }, - ) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") - - def post(self, text, title, media=None): - if not media: - return self.post_text(text, title) - else: - media_id = self.upload_image(media) - - if media_id: - return self.post_text(text, title, media_id=media_id) - else: - self.log_error("LinkedIn: Failed to upload media") - - def upload_image(self, media): - media = get_file_path(media) - register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" - body = { - "registerUploadRequest": { - "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], - "owner": "urn:li:organization:{0}".format(self.company_id), - "serviceRelationships": [ - {"relationshipType": "OWNER", "identifier": "urn:li:userGeneratedContent"} - ], - } - } - headers = self.get_headers() - response = self.http_post(url=register_url, body=body, headers=headers) - - if response.status_code == 200: - response = response.json() - asset = response["value"]["asset"] - upload_url = response["value"]["uploadMechanism"][ - "com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest" - ]["uploadUrl"] - headers["Content-Type"] = "image/jpeg" - response = self.http_post(upload_url, headers=headers, data=open(media, "rb")) - if response.status_code < 200 and response.status_code > 299: - frappe.throw( - _("Error While Uploading Image"), - title="{0} {1}".format(response.status_code, response.reason), - ) - return None - return asset - - return None - - def post_text(self, text, title, media_id=None): - url = "https://api.linkedin.com/v2/shares" - headers = self.get_headers() - headers["X-Restli-Protocol-Version"] = "2.0.0" - headers["Content-Type"] = "application/json; charset=UTF-8" - - body = { - "distribution": {"linkedInDistributionTarget": {}}, - "owner": "urn:li:organization:{0}".format(self.company_id), - "subject": title, - "text": {"text": text}, - } - - reference_url = self.get_reference_url(text) - if reference_url: - body["content"] = {"contentEntities": [{"entityLocation": reference_url}]} - - if media_id: - body["content"] = {"contentEntities": [{"entity": media_id}], "shareMediaCategory": "IMAGE"} - - response = self.http_post(url=url, headers=headers, body=body) - return response - - def http_post(self, url, headers=None, body=None, data=None): - try: - response = requests.post(url=url, json=body, data=data, headers=headers) - if response.status_code not in [201, 200]: - raise - - except Exception as e: - self.api_error(response) - - return response - - def get_headers(self): - return {"Authorization": "Bearer {}".format(self.access_token)} - - def get_reference_url(self, text): - import re - - regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" - urls = re.findall(regex_url, text) - if urls: - return urls[0] - - def delete_post(self, post_id): - try: - response = requests.delete( - url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), - headers=self.get_headers(), - ) - if response.status_code != 200: - raise - except Exception: - self.api_error(response) - - def get_post(self, post_id): - url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format( - self.company_id, post_id - ) - - try: - response = requests.get(url=url, headers=self.get_headers()) - if response.status_code != 200: - raise - - except Exception: - self.api_error(response) - - response = frappe.parse_json(response.content.decode()) - if len(response.elements): - return response.elements[0] - - return None - - def api_error(self, response): - content = frappe.parse_json(response.content.decode()) - - if response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized")) - elif response.status_code == 403: - frappe.msgprint(_("You didn't have permission to access this API")) - frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied")) - else: - frappe.throw(response.reason, title=response.status_code) - - -@frappe.whitelist(allow_guest=True) -def callback(code=None, error=None, error_description=None): - if not error: - linkedin_settings = frappe.get_doc("LinkedIn Settings") - linkedin_settings.get_access_token(code) - linkedin_settings.get_member_profile() - frappe.db.commit() - else: - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py deleted file mode 100644 index 09732e405ee..00000000000 --- a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestLinkedInSettings(unittest.TestCase): - pass diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js deleted file mode 100644 index 71d044c38d8..00000000000 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt -frappe.ui.form.on('Social Media Post', { - validate: function(frm) { - if (frm.doc.linkedin === 0) { - frappe.throw(__("Select atleast one Social Media Platform to Share on.")); - } - if (frm.doc.scheduled_time) { - let scheduled_time = new Date(frm.doc.scheduled_time); - let date_time = new Date(); - if (scheduled_time.getTime() < date_time.getTime()) { - frappe.throw(__("Scheduled Time must be a future time.")); - } - } - frm.trigger('validate_tweet_length'); - }, - - text: function(frm) { - if (frm.doc.text) { - frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`); - frm.refresh_field('text'); - frm.trigger('validate_tweet_length'); - } - }, - - validate_tweet_length: function(frm) { - if (frm.doc.text && frm.doc.text.length > 280) { - frappe.throw(__("Tweet length Must be less than 280.")); - } - }, - - onload: function(frm) { - frm.trigger('make_dashboard'); - }, - - make_dashboard: function(frm) { - if (frm.doc.post_status == "Posted") { - frappe.call({ - doc: frm.doc, - method: 'get_post', - freeze: true, - callback: (r) => { - if (!r.message) { - return; - } - - let datasets = [], colors = []; - if (r.message && r.message.linkedin) { - colors.push('#0077b5'); - datasets.push({ - name: 'LinkedIn', - values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount] - }); - } - - if (datasets.length) { - frm.dashboard.render_graph({ - data: { - labels: ['Likes', 'Retweets/Shares'], - datasets: datasets - }, - - title: __("Post Metrics"), - type: 'bar', - height: 300, - colors: colors - }); - } - } - }); - } - }, - - refresh: function(frm) { - frm.trigger('text'); - - if (frm.doc.docstatus === 1) { - if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) { - frm.trigger('add_post_btn'); - } - if (frm.doc.post_status !='Deleted') { - frm.add_custom_button(__('Delete Post'), function() { - frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'), - function() { - frappe.call({ - doc: frm.doc, - method: 'delete_post', - freeze: true, - callback: () => { - frm.reload_doc(); - } - }); - } - ); - }); - } - - if (frm.doc.post_status !='Deleted') { - let html=''; - if (frm.doc.linkedin) { - let color = frm.doc.linkedin_post_id ? "green" : "red"; - let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += `
- LinkedIn : ${status} -
` ; - } - html = `
${html}
`; - frm.dashboard.set_headline_alert(html); - } - } - }, - - add_post_btn: function(frm) { - frm.add_custom_button(__('Post Now'), function() { - frappe.call({ - doc: frm.doc, - method: 'post', - freeze: true, - callback: function() { - frm.reload_doc(); - } - }); - }); - } -}); diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json deleted file mode 100644 index 1e3e01c36fe..00000000000 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "actions": [], - "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", - "creation": "2020-01-30 11:53:13.872864", - "doctype": "DocType", - "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "campaign_name", - "scheduled_time", - "post_status", - "column_break_6", - "linkedin", - "linkedin_post_id", - "linkedin_section", - "linkedin_post", - "column_break_15", - "attachments_section", - "image", - "amended_from" - ], - "fields": [ - { - "fieldname": "image", - "fieldtype": "Attach Image", - "label": "Image" - }, - { - "default": "1", - "fieldname": "linkedin", - "fieldtype": "Check", - "label": "LinkedIn" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Social Media Post", - "print_hide": 1, - "read_only": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "post_status", - "fieldtype": "Select", - "label": "Post Status", - "no_copy": 1, - "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError", - "read_only": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "linkedin_post_id", - "fieldtype": "Data", - "hidden": 1, - "label": "LinkedIn Post Id", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "campaign_name", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Campaign", - "options": "Campaign" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "label": "Share On" - }, - { - "collapsible": 1, - "depends_on": "eval:doc.linkedin==1", - "fieldname": "linkedin_section", - "fieldtype": "Section Break", - "label": "LinkedIn" - }, - { - "collapsible": 1, - "fieldname": "attachments_section", - "fieldtype": "Section Break", - "label": "Attachments" - }, - { - "fieldname": "linkedin_post", - "fieldtype": "Text", - "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1" - }, - { - "fieldname": "column_break_15", - "fieldtype": "Column Break" - }, - { - "allow_on_submit": 1, - "fieldname": "scheduled_time", - "fieldtype": "Datetime", - "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"" - }, - { - "fieldname": "title", - "fieldtype": "Data", - "label": "Title", - "reqd": 1 - } - ], - "is_submittable": 1, - "links": [], - "modified": "2023-09-14 11:24:29.105683", - "modified_by": "Administrator", - "module": "CRM", - "name": "Social Media Post", - "naming_rule": "Expression", - "owner": "Administrator", - "permissions": [ - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "title", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py deleted file mode 100644 index 9615a8389ca..00000000000 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import datetime - -import frappe -from frappe import _ -from frappe.model.document import Document - - -class SocialMediaPost(Document): - def validate(self): - if not self.linkedin: - frappe.throw(_("Select atleast one Social Media Platform to Share on.")) - - if self.scheduled_time: - current_time = frappe.utils.now_datetime() - scheduled_time = frappe.utils.get_datetime(self.scheduled_time) - if scheduled_time < current_time: - frappe.throw(_("Scheduled Time must be a future time.")) - - if self.text and len(self.text) > 280: - frappe.throw(_("Tweet length must be less than 280.")) - - def submit(self): - if self.scheduled_time: - self.post_status = "Scheduled" - super(SocialMediaPost, self).submit() - - def on_cancel(self): - self.db_set("post_status", "Cancelled") - - @frappe.whitelist() - def delete_post(self): - if self.linkedin and self.linkedin_post_id: - linkedin = frappe.get_doc("LinkedIn Settings") - linkedin.delete_post(self.linkedin_post_id) - - self.db_set("post_status", "Deleted") - - @frappe.whitelist() - def get_post(self): - response = {} - if self.linkedin and self.linkedin_post_id: - linkedin = frappe.get_doc("LinkedIn Settings") - response["linkedin"] = linkedin.get_post(self.linkedin_post_id) - return response - - @frappe.whitelist() - def post(self): - try: - if self.linkedin and not self.linkedin_post_id: - linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image) - self.db_set("linkedin_post_id", linkedin_post.headers["X-RestLi-Id"]) - self.db_set("post_status", "Posted") - - except Exception: - self.db_set("post_status", "Error") - self.log_error("Social posting failed") - - -def process_scheduled_social_media_posts(): - posts = frappe.get_all( - "Social Media Post", - filters={"post_status": "Scheduled", "docstatus": 1}, - fields=["name", "scheduled_time"], - ) - start = frappe.utils.now_datetime() - end = start + datetime.timedelta(minutes=10) - for post in posts: - if post.scheduled_time: - post_time = frappe.utils.get_datetime(post.scheduled_time) - if post_time > start and post_time <= end: - sm_post = frappe.get_doc("Social Media Post", post.name) - sm_post.post() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js deleted file mode 100644 index a8c8272ad08..00000000000 --- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js +++ /dev/null @@ -1,11 +0,0 @@ -frappe.listview_settings['Social Media Post'] = { - add_fields: ["status", "post_status"], - get_indicator: function(doc) { - return [__(doc.post_status), { - "Scheduled": "orange", - "Posted": "green", - "Error": "red", - "Deleted": "red" - }[doc.post_status]]; - } -} diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py deleted file mode 100644 index 75744767dca..00000000000 --- a/erpnext/crm/doctype/social_media_post/test_social_media_post.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestSocialMediaPost(unittest.TestCase): - pass diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index b107df76f8f..4b5b9af714b 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -122,131 +122,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Campaign", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Campaign", - "link_count": 0, - "link_to": "Campaign", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Campaign", - "link_count": 0, - "link_to": "Email Campaign", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Social Media Post", - "link_count": 0, - "link_to": "Social Media Post", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "SMS Center", - "link_count": 0, - "link_to": "SMS Center", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "SMS Log", - "link_count": 0, - "link_to": "SMS Log", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Group", - "link_count": 0, - "link_to": "Email Group", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "CRM Settings", - "link_count": 0, - "link_to": "CRM Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "SMS Settings", - "link_count": 0, - "link_to": "SMS Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Twitter Settings", - "link_count": 0, - "link_to": "Twitter Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "LinkedIn Settings", - "link_count": 0, - "link_to": "LinkedIn Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -450,9 +325,101 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM Settings", + "link_count": 0, + "link_to": "CRM Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_count": 0, + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 0, + "link_to": "Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Campaign", + "link_count": 0, + "link_to": "Email Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Center", + "link_count": 0, + "link_to": "SMS Center", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Log", + "link_count": 0, + "link_to": "SMS Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_count": 0, + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2023-05-26 16:49:04.298122", + "modified": "2023-09-14 12:11:03.968048", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -463,7 +430,7 @@ "quick_lists": [], "restrict_to_domain": "", "roles": [], - "sequence_id": 10.0, + "sequence_id": 17.0, "shortcuts": [ { "color": "Blue", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 41db6b3a725..c7398cce999 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -423,9 +423,6 @@ scheduler_events = { "erpnext.stock.reorder_item.reorder_item", ], }, - "all": [ - "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", - ], "hourly": [ "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.project_status_update_reminder", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 22c37159cdd..7a07f5679a7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -340,6 +340,8 @@ erpnext.patches.v15_0.remove_exotel_integration erpnext.patches.v14_0.single_to_multi_dunning execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) execute:frappe.delete_doc('DocType', 'Twitter Settings', ignore_missing=True) +execute:frappe.delete_doc('DocType', 'LinkedIn Settings', ignore_missing=True) +execute:frappe.delete_doc('DocType', 'Social Media Post', ignore_missing=True) erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow erpnext.patches.v15_0.delete_woocommerce_settings_doctype # below migration patch should always run last From 2f7b3bbfad76948bd00e7087854502d61d8eab07 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 14 Sep 2023 14:46:45 +0530 Subject: [PATCH 51/55] ci: fix patch test (#37079) * ci: fix patch test * ci: use v13 DB as starting point for patch * ci: remove payments from patch test * ci: print bench logs after tests * ci: restart bench on each update * ci: patch test v13db > v14 > develop and when v15 is out v13db > v14 > v15 > develop --- .github/helper/install.sh | 2 +- .github/workflows/patch.yml | 62 ++++++++++++++-------- .github/workflows/server-tests-mariadb.yml | 4 ++ 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 48337cee640..d1a97f87ffb 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -68,6 +68,6 @@ if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi wait $wkpid -bench start &> bench_run_logs.txt & +bench start &>> ~/frappe-bench/bench_start.log & CI=Yes bench build --app frappe & bench --site test_site reinstall --yes diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index aae2928bf0d..a633414fb9c 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -23,7 +23,7 @@ jobs: services: mysql: - image: mariadb:10.3 + image: mariadb:10.6 env: MARIADB_ROOT_PASSWORD: 'root' ports: @@ -45,9 +45,7 @@ jobs: - name: Setup Python uses: "actions/setup-python@v4" with: - python-version: | - 3.7 - 3.10 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 @@ -102,40 +100,58 @@ jobs: - name: Run Patch Tests run: | cd ~/frappe-bench/ - wget https://erpnext.com/files/v10-erpnext.sql.gz - bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz + bench remove-app payments --force + jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json + mv tmp.json ~/frappe-bench/sites/test_site/site_config.json + + wget https://erpnext.com/files/v13-erpnext.sql.gz + bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git - for version in $(seq 12 13) - do - echo "Updating to v$version" - branch_name="version-$version-hotfix" - git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name - git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name + function update_to_version() { + version=$1 - git -C "apps/frappe" checkout -q -f $branch_name - git -C "apps/erpnext" checkout -q -f $branch_name + branch_name="version-$version-hotfix" + echo "Updating to v$version" - rm -rf ~/frappe-bench/env - bench setup env --python python3.7 - bench pip install -e ./apps/payments - bench pip install -e ./apps/erpnext + # Fetch and checkout branches + git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name + git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name + git -C "apps/frappe" checkout -q -f $branch_name + git -C "apps/erpnext" checkout -q -f $branch_name - bench --site test_site migrate - done + # Resetup env and install apps + pgrep honcho | xargs kill + rm -rf ~/frappe-bench/env + bench -v setup env + bench pip install -e ./apps/erpnext + bench start &>> ~/frappe-bench/bench_start.log & + bench --site test_site migrate + } + + update_to_version 14 echo "Updating to latest version" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" rm -rf ~/frappe-bench/env - bench -v setup env --python python3.10 - bench pip install -e ./apps/payments + bench -v setup env bench pip install -e ./apps/erpnext bench --site test_site migrate - bench --site test_site install-app payments + + - name: Show bench output + if: ${{ always() }} + run: | + cd ~/frappe-bench + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 2ce1125456e..559be06993e 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -123,6 +123,10 @@ jobs: CI_BUILD_ID: ${{ github.run_id }} ORCHESTRATOR_URL: http://test-orchestrator.frappe.io + - name: Show bench output + if: ${{ always() }} + run: cat ~/frappe-bench/bench_start.log || true + - name: Upload coverage data uses: actions/upload-artifact@v3 with: From 08aaf22b2aef3cdac0c3a982c9fc7facb8e35bb5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:09:48 +0530 Subject: [PATCH 52/55] fix: precision issue and column name (backport #37073) (#37089) fix: precision issue and column name (#37073) (cherry picked from commit f2395a92971f48904e88afb929285def7ba221e4) Co-authored-by: rohitwaghchaure --- .../requested_items_to_order_and_receive.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index 21241e08603..07187352eb7 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -7,7 +7,7 @@ import copy import frappe from frappe import _ from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import date_diff, flt, getdate +from frappe.utils import cint, date_diff, flt, getdate def execute(filters=None): @@ -47,8 +47,10 @@ def get_data(filters): mr.transaction_date.as_("date"), mr_item.schedule_date.as_("required_date"), mr_item.item_code.as_("item_code"), - Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), - Coalesce(mr_item.stock_uom, "").as_("uom"), + Sum(Coalesce(mr_item.qty, 0)).as_("qty"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"), + Coalesce(mr_item.uom, "").as_("uom"), + Coalesce(mr_item.stock_uom, "").as_("stock_uom"), Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_( @@ -96,7 +98,7 @@ def get_conditions(filters, query, mr, mr_item): def update_qty_columns(row_to_update, data_row): - fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] + fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] for field in fields: row_to_update[field] += flt(data_row[field]) @@ -104,16 +106,20 @@ def update_qty_columns(row_to_update, data_row): def prepare_data(data, filters): """Prepare consolidated Report data and Chart data""" material_request_map, item_qty_map = {}, {} + precision = cint(frappe.db.get_default("float_precision")) or 2 for row in data: # item wise map for charts if not row["item_code"] in item_qty_map: item_qty_map[row["item_code"]] = { - "qty": row["qty"], - "ordered_qty": row["ordered_qty"], - "received_qty": row["received_qty"], - "qty_to_receive": row["qty_to_receive"], - "qty_to_order": row["qty_to_order"], + "qty": flt(row["stock_qty"], precision), + "stock_qty": flt(row["stock_qty"], precision), + "stock_uom": row["stock_uom"], + "uom": row["uom"], + "ordered_qty": flt(row["ordered_qty"], precision), + "received_qty": flt(row["received_qty"], precision), + "qty_to_receive": flt(row["qty_to_receive"], precision), + "qty_to_order": flt(row["qty_to_order"], precision), } else: item_entry = item_qty_map[row["item_code"]] @@ -200,21 +206,34 @@ def get_columns(filters): {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 100, }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Data", + "width": 100, + }, ] ) columns.extend( [ { - "label": _("Stock Qty"), + "label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", - "width": 120, + "width": 140, + "convertible": "qty", + }, + { + "label": _("Qty in Stock UOM"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 140, "convertible": "qty", }, { From f2b0ac6868387611d9d6d00274fea6655aceb9c0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:19:27 +0530 Subject: [PATCH 53/55] refactor: move `unreconcile` btn inside a drop down --- erpnext/public/js/utils/unreconcile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index acc77a64b01..bbdd51d6e54 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -19,7 +19,7 @@ erpnext.accounts.unreconcile_payments = { if (r.message) { frm.add_custom_button(__("Un-Reconcile"), function() { erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); - }); + }, __('Actions')); } } }); From 94ce43b0d5d131e31e6fc01ac06cfa748d73caed Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:21:50 +0530 Subject: [PATCH 54/55] refactor: add `unreconcile` btn to purchase invoice --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index efe97415a55..c8c9ad1b3a9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -161,6 +161,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } unblock_invoice() { From 61778d505804df007a04583b5e7710eb8878bd9a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 15 Sep 2023 16:51:56 +0530 Subject: [PATCH 55/55] ci: restart bench before final migrate (#37104) Also remove few patches which are now handled automatically --- .github/workflows/patch.yml | 2 ++ erpnext/patches.txt | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index a633414fb9c..07b8de7a900 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -139,9 +139,11 @@ jobs: git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" + pgrep honcho | xargs kill rm -rf ~/frappe-bench/env bench -v setup env bench pip install -e ./apps/erpnext + bench start &>> ~/frappe-bench/bench_start.log & bench --site test_site migrate diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7a07f5679a7..d0ee2e4dc4e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,9 +339,6 @@ execute:frappe.defaults.clear_default("fiscal_year") erpnext.patches.v15_0.remove_exotel_integration erpnext.patches.v14_0.single_to_multi_dunning execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) -execute:frappe.delete_doc('DocType', 'Twitter Settings', ignore_missing=True) -execute:frappe.delete_doc('DocType', 'LinkedIn Settings', ignore_missing=True) -execute:frappe.delete_doc('DocType', 'Social Media Post', ignore_missing=True) erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow erpnext.patches.v15_0.delete_woocommerce_settings_doctype # below migration patch should always run last