feat: repost GL Entries only

This commit is contained in:
Rohit Waghchaure
2025-11-20 18:54:01 +05:30
parent 8f01e89d76
commit b01f872f7d
6 changed files with 217 additions and 15 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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");

View File

@@ -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",

View File

@@ -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()

View File

@@ -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" });
});
},
};