From b01f872f7def1ca83061363f8bd17c656774b8e7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Nov 2025 18:54:01 +0530 Subject: [PATCH] feat: repost GL Entries only --- erpnext/desktop_icon/home.json | 2 +- .../purchase_receipt/test_purchase_receipt.py | 54 ++++++++ .../repost_item_valuation.js | 8 +- .../repost_item_valuation.json | 20 ++- .../repost_item_valuation.py | 121 ++++++++++++++++-- .../repost_item_valuation_list.js | 27 ++++ 6 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation_list.js diff --git a/erpnext/desktop_icon/home.json b/erpnext/desktop_icon/home.json index ba1fdafb644..ab4014de489 100644 --- a/erpnext/desktop_icon/home.json +++ b/erpnext/desktop_icon/home.json @@ -10,7 +10,7 @@ "label": "Home", "link_to": "Home", "link_type": "Workspace", - "modified": "2025-11-18 12:06:56.506311", + "modified": "2025-11-20 16:09:28.269913", "modified_by": "Administrator", "name": "Home", "owner": "Administrator", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 48acf7b0649..6fac45ea255 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4536,6 +4536,60 @@ class TestPurchaseReceipt(IntegrationTestCase): if row.account == expense_contra_account: self.assertEqual(row.credit, 1000) + def test_repost_gl_entries(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = "Test Item for Repost GL Entries" + make_item(item, {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + account = "Reposting Adjustment - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Reposting Adjustment", + "parent_account": "Stock Expenses - TCP1", + "company": company, + "is_group": 0, + "account_type": "Expense Account", + } + ).insert() + + se = make_stock_entry( + item_code=item, + qty=10, + rate=100, + company=company, + target="Stores - TCP1", + ) + + gl_entries = get_gl_entries(se.doctype, se.name) + for row in gl_entries: + self.assertTrue(row.account in ["Stock In Hand - TCP1", "Stock Adjustment - TCP1"]) + + se.items[0].db_set("expense_account", account) + se.reload() + + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Transaction", + "voucher_type": se.doctype, + "voucher_no": se.name, + "posting_date": se.posting_date, + "posting_time": se.posting_time, + "company": se.company, + "repost_only_accounting_ledgers": 1, + } + ) + + repost_doc.submit() + + gl_entries = get_gl_entries(se.doctype, se.name) + for row in gl_entries: + self.assertTrue(row.account in ["Stock In Hand - TCP1", account]) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index e6547ad6f35..0f64949f621 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -41,7 +41,9 @@ frappe.ui.form.on("Repost Item Valuation", { }); } - frm.trigger("setup_realtime_progress"); + if (frm.doc.status !== "Completed") { + frm.trigger("setup_realtime_progress"); + } }, based_on: function (frm) { @@ -84,7 +86,9 @@ frappe.ui.form.on("Repost Item Valuation", { }).addClass("btn-primary"); } - frm.trigger("show_reposting_progress"); + if (frm.doc.status !== "Completed") { + frm.trigger("show_reposting_progress"); + } if (frm.doc.status === "Queued" && frm.doc.docstatus === 1) { frm.trigger("execute_reposting"); diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index c765e389e72..23701e7c94e 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "autoname": "hash", "creation": "2022-01-11 15:03:38.273179", "doctype": "DocType", @@ -16,6 +17,8 @@ "column_break_5", "status", "company", + "reposting_reference", + "repost_only_accounting_ledgers", "allow_negative_stock", "via_landed_cost_voucher", "allow_zero_rate", @@ -228,13 +231,28 @@ "fieldname": "recreate_stock_ledgers", "fieldtype": "Check", "label": "Recreate Stock Ledgers" + }, + { + "depends_on": "repost_only_accounting_ledgers", + "fieldname": "reposting_reference", + "fieldtype": "Data", + "label": "Reposting Reference", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.based_on === \"Transaction\"", + "description": "Stock Ledgers won\u2019t be reposted.", + "fieldname": "repost_only_accounting_ledgers", + "fieldtype": "Check", + "label": "Repost Only Accounting Ledgers" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-03-31 12:38:20.566196", + "modified": "2025-11-20 18:24:48.808526", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index b7802290262..9ab616c94ca 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -1,6 +1,8 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ from frappe.desk.form.load import get_attachments @@ -48,7 +50,9 @@ class RepostItemValuation(Document): posting_date: DF.Date posting_time: DF.Time | None recreate_stock_ledgers: DF.Check + repost_only_accounting_ledgers: DF.Check reposting_data_file: DF.Attach | None + reposting_reference: DF.Data | None status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"] total_reposting_count: DF.Int via_landed_cost_voucher: DF.Check @@ -70,6 +74,7 @@ class RepostItemValuation(Document): ) def validate(self): + self.reset_repost_only_accounting_ledgers() self.set_company() self.validate_period_closing_voucher() self.set_status(write=False) @@ -78,6 +83,10 @@ class RepostItemValuation(Document): self.reset_recreate_stock_ledgers() self.validate_recreate_stock_ledgers() + def reset_repost_only_accounting_ledgers(self): + if self.repost_only_accounting_ledgers and self.based_on != "Transaction": + self.repost_only_accounting_ledgers = 0 + def validate_recreate_stock_ledgers(self): if not self.recreate_stock_ledgers: return @@ -242,11 +251,39 @@ class RepostItemValuation(Document): self.distinct_item_and_warehouse = None self.items_to_be_repost = None self.gl_reposting_index = 0 + self.total_reposting_count = 0 self.clear_attachment() self.db_update() + def skipped_similar_reposts(self): + repost_entries = frappe.get_all( + "Repost Item Valuation", + filters={ + "based_on": "Transaction", + "voucher_type": self.voucher_type, + "voucher_no": self.voucher_no, + "docstatus": 1, + "repost_only_accounting_ledgers": 1, + "status": "Queued", + "reposting_reference": ("is", "set"), + }, + fields=["name", "reposting_reference"], + ) + + for entry in repost_entries: + if ( + frappe.db.get_value("Repost Item Valuation", entry.reposting_reference, "status") + == "Completed" + ): + frappe.db.set_value("Repost Item Valuation", entry.name, "status", "Skipped") + def deduplicate_similar_repost(self): """Deduplicate similar reposts based on item-warehouse-posting combination.""" + + if self.repost_only_accounting_ledgers: + self.skipped_similar_reposts() + return + if self.based_on != "Item and Warehouse": return @@ -284,6 +321,19 @@ class RepostItemValuation(Document): doc.update_stock_ledger(allow_negative_stock=True) +@frappe.whitelist() +def bulk_restart_reposting(names): + names = json.loads(names) + for name in names: + doc = frappe.get_doc("Repost Item Valuation", name) + if doc.status != "Failed": + continue + + doc.restart_reposting() + + frappe.msgprint(_("Repost Item Valuation restarted for selected failed records.")) + + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -304,7 +354,9 @@ def repost(doc): if doc.recreate_stock_ledgers: doc.recreate_stock_ledger_entries() - repost_sl_entries(doc) + if not doc.repost_only_accounting_ledgers: + repost_sl_entries(doc) + repost_gl_entries(doc) doc.set_status("Completed") @@ -393,15 +445,34 @@ def repost_gl_entries(doc): if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): return + if doc.repost_only_accounting_ledgers and doc.based_on == "Transaction": + transactions = [(doc.voucher_type, doc.voucher_no)] + repost_gle_for_stock_vouchers( + transactions, + doc.posting_date, + doc.company, + repost_doc=doc, + ) + return + # directly modified transactions directly_dependent_transactions = _get_directly_dependent_vouchers(doc) repost_affected_transaction = get_affected_transactions(doc) - repost_gle_for_stock_vouchers( - directly_dependent_transactions + list(repost_affected_transaction), - doc.posting_date, - doc.company, - repost_doc=doc, - ) + + transactions = directly_dependent_transactions + list(repost_affected_transaction) + if doc.based_on == "Item and Warehouse" and not doc.repost_only_accounting_ledgers: + make_reposting_for_accounting_ledgers( + transactions, + doc.company, + repost_doc=doc, + ) + else: + repost_gle_for_stock_vouchers( + transactions, + doc.posting_date, + doc.company, + repost_doc=doc, + ) def _get_directly_dependent_vouchers(doc): @@ -477,14 +548,17 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc("Repost Item Valuation", row.name) + if ( + doc.repost_only_accounting_ledgers + and doc.reposting_reference + and frappe.db.get_value("Repost Item Valuation", doc.reposting_reference, "status") != "Completed" + ): + continue + if doc.status in ("Queued", "In Progress"): repost(doc) doc.deduplicate_similar_repost() - riv_entries = get_repost_item_valuation_entries() - if riv_entries: - return - def get_repost_item_valuation_entries(): return frappe.db.sql( @@ -529,3 +603,28 @@ def execute_repost_item_valuation(): "name", ): frappe.get_doc("Scheduled Job Type", name).enqueue(force=True) + + +def make_reposting_for_accounting_ledgers(transactions, company, repost_doc): + for voucher_type, voucher_no in transactions: + if frappe.db.exists( + "Repost Item Valuation", + { + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "docstatus": 1, + "reposting_reference": repost_doc.name, + "repost_only_accounting_ledgers": 1, + "status": "Queued", + }, + ): + continue + + new_repost_doc = frappe.new_doc("Repost Item Valuation") + new_repost_doc.company = company + new_repost_doc.voucher_type = voucher_type + new_repost_doc.voucher_no = voucher_no + new_repost_doc.repost_only_accounting_ledgers = 1 + new_repost_doc.reposting_reference = repost_doc.name + new_repost_doc.flags.ignore_permissions = True + new_repost_doc.submit() diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation_list.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation_list.js new file mode 100644 index 00000000000..a963f27d757 --- /dev/null +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation_list.js @@ -0,0 +1,27 @@ +frappe.listview_settings["Repost Item Valuation"] = { + add_fields: ["status", "name", "voucher_type", "voucher_no"], + get_indicator: function (doc) { + if (doc.status === "Completed") { + // Closed + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.status === "Queued") { + // on hold + return [__("Queued"), "red", "status,=,Queued"]; + } else if (doc.status === "In Progress") { + // on hold + return [__("In Progress"), "orange", "status,=,In Progress"]; + } else if (doc.status === "Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } else { + return [__(doc.status), "blue", true]; + } + }, + onload: function (listview) { + var method = + "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.bulk_restart_reposting"; + + listview.page.add_action_item(__("Restart Failed Entries"), () => { + listview.call_for_selected_items(method, { status: "Failed" }); + }); + }, +};