feat: Repost Payment Ledger entries for vouchers
primarily intended to manually correct PLE entries for vouchers
affected by Item Value repost-https://github.com/frappe/erpnext/pull/32567
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/__init__.py b/erpnext/accounts/doctype/repost_payment_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/__init__.py
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js
new file mode 100644
index 0000000..6801408
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Repost Payment Ledger', {
+ setup: function(frm) {
+ frm.set_query("voucher_type", () => {
+ return {
+ filters: {
+ name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
+ }
+ };
+ });
+
+ frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
+ return {
+ filters: {
+ name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
+ }
+ }
+ }
+
+ frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
+ if (doc.company) {
+ return {
+ filters: {
+ company: doc.company,
+ docstatus: 1
+ }
+ }
+ }
+ }
+
+ },
+ refresh: function(frm) {
+
+ if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) {
+ frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status."));
+ var btn_label = __("Repost in background")
+
+ frm.add_custom_button(btn_label, () => {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger',
+ args: {
+ docname: frm.doc.name,
+ }
+ });
+ frappe.msgprint(__('Reposting in the background.'));
+ });
+ }
+
+ }
+});
+
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
new file mode 100644
index 0000000..5175fd1
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
@@ -0,0 +1,159 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2022-10-19 21:59:33.553852",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "filters_section",
+ "company",
+ "posting_date",
+ "column_break_4",
+ "voucher_type",
+ "add_manually",
+ "status_section",
+ "repost_status",
+ "repost_error_log",
+ "selected_vouchers_section",
+ "repost_vouchers",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Repost Payment Ledger",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "selected_vouchers_section",
+ "fieldtype": "Section Break",
+ "label": "Vouchers"
+ },
+ {
+ "fieldname": "filters_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "repost_vouchers",
+ "fieldtype": "Table",
+ "label": "Selected Vouchers",
+ "options": "Repost Payment Ledger Items"
+ },
+ {
+ "fieldname": "repost_status",
+ "fieldtype": "Select",
+ "label": "Repost Status",
+ "options": "\nQueued\nFailed\nCompleted",
+ "read_only": 1
+ },
+ {
+ "fieldname": "status_section",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "default": "0",
+ "description": "Ignore Voucher Type filter and Select Vouchers Manually",
+ "fieldname": "add_manually",
+ "fieldtype": "Check",
+ "label": "Add Manually"
+ },
+ {
+ "depends_on": "eval:doc.repost_error_log",
+ "fieldname": "repost_error_log",
+ "fieldtype": "Long Text",
+ "label": "Repost Error Log"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-11-08 07:38:40.079038",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Payment Ledger",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
new file mode 100644
index 0000000..9f6828f
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils.background_jobs import is_job_queued
+
+from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
+
+VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
+
+
+def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
+ if voucher_type and voucher_no and gle_map:
+ _delete_pl_entries(voucher_type, voucher_no)
+ create_payment_ledger_entry(gle_map, cancel=0)
+
+
+@frappe.whitelist()
+def start_payment_ledger_repost(docname=None):
+ """
+ Repost Payment Ledger Entries for Vouchers through Background Job
+ """
+ if docname:
+ repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
+ if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
+ try:
+ for entry in repost_doc.repost_vouchers:
+ doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
+
+ if doc.doctype in ["Payment Entry", "Journal Entry"]:
+ gle_map = doc.build_gl_map()
+ else:
+ gle_map = doc.get_gl_entries()
+
+ repost_ple_for_voucher(entry.voucher_type, entry.voucher_no, gle_map)
+
+ frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "")
+ frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed")
+ except Exception as e:
+ frappe.db.rollback()
+
+ traceback = frappe.get_traceback()
+ if traceback:
+ message = "Traceback: <br>" + traceback
+ frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
+
+ frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Failed")
+
+
+class RepostPaymentLedger(Document):
+ def __init__(self, *args, **kwargs):
+ super(RepostPaymentLedger, self).__init__(*args, **kwargs)
+ self.vouchers = []
+
+ def before_validate(self):
+ self.load_vouchers_based_on_filters()
+ self.set_status()
+
+ def load_vouchers_based_on_filters(self):
+ if not self.add_manually:
+ self.repost_vouchers.clear()
+ self.get_vouchers()
+ self.extend("repost_vouchers", copy.deepcopy(self.vouchers))
+
+ def get_vouchers(self):
+ self.vouchers.clear()
+
+ filter_on_voucher_types = [self.voucher_type] if self.voucher_type else VOUCHER_TYPES
+
+ for vtype in filter_on_voucher_types:
+ doc = qb.DocType(vtype)
+ doctype_name = ConstantColumn(vtype)
+ query = (
+ qb.from_(doc)
+ .select(doctype_name.as_("voucher_type"), doc.name.as_("voucher_no"))
+ .where(
+ (doc.docstatus == 1)
+ & (doc.company == self.company)
+ & (doc.posting_date.gte(self.posting_date))
+ )
+ )
+ entries = query.run(as_dict=True)
+ self.vouchers.extend(entries)
+
+ def set_status(self):
+ if self.docstatus == 0:
+ self.repost_status = "Queued"
+
+ def on_submit(self):
+ execute_repost_payment_ledger(self.name)
+ frappe.msgprint(_("Repost started in the background"))
+
+
+@frappe.whitelist()
+def execute_repost_payment_ledger(docname):
+ """Repost Payment Ledger Entries by background job."""
+
+ job_name = "payment_ledger_repost_" + docname
+
+ if not is_job_queued(job_name):
+ frappe.enqueue(
+ method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
+ docname=docname,
+ is_async=True,
+ job_name=job_name,
+ )
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js
new file mode 100644
index 0000000..e045184
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings["Repost Payment Ledger"] = {
+ add_fields: ["repost_status"],
+ get_indicator: function(doc) {
+ var colors = {
+ 'Queued': 'orange',
+ 'Completed': 'green',
+ 'Failed': 'red',
+ };
+ let status = doc.repost_status;
+ return [__(status), colors[status], 'status,=,'+status];
+ },
+};
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py
new file mode 100644
index 0000000..781726a
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestRepostPaymentLedger(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/repost_payment_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_payment_ledger_items/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger_items/__init__.py
diff --git a/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json
new file mode 100644
index 0000000..93005ee
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json
@@ -0,0 +1,35 @@
+{
+ "actions": [],
+ "creation": "2022-10-20 10:44:18.796489",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "voucher_type",
+ "voucher_no"
+ ],
+ "fields": [
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-10-28 14:47:11.838109",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Payment Ledger Items",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py
new file mode 100644
index 0000000..fb19e84
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostPaymentLedgerItems(Document):
+ pass