mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 14:41:53 +00:00
* feat: weekly auto-repost of incorrect stock valuation entries (#56637)
(cherry picked from commit adae0bd732)
# Conflicts:
# erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
* chore: fix conflicts
Removed merge conflict markers and cleaned up code.
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -492,6 +492,7 @@ scheduler_events = {
|
|||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||||
|
"erpnext.stock.doctype.stock_reposting_settings.stock_reposting_settings.repost_incorrect_valuation_entries",
|
||||||
],
|
],
|
||||||
"monthly_long": [
|
"monthly_long": [
|
||||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"column_break_itvd",
|
"column_break_itvd",
|
||||||
"enable_separate_reposting_for_gl",
|
"enable_separate_reposting_for_gl",
|
||||||
"errors_notification_section",
|
"errors_notification_section",
|
||||||
"notify_reposting_error_to_role"
|
"notify_reposting_error_to_role",
|
||||||
|
"auto_reposting_section",
|
||||||
|
"repost_incorrect_valuation_entries"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -113,12 +115,24 @@
|
|||||||
"fieldname": "do_not_fetch_incoming_rate_from_serial_no",
|
"fieldname": "do_not_fetch_incoming_rate_from_serial_no",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Do not fetch incoming rate from Serial No"
|
"label": "Do not fetch incoming rate from Serial No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "auto_reposting_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Auto Reposting of Incorrect Valuation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If enabled, a weekly scheduler scans the Stock Ledger Variance for item-warehouses with incorrect valuation in the current financial year and auto-creates Item & Warehouse based reposts to fix them.",
|
||||||
|
"fieldname": "repost_incorrect_valuation_entries",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Auto Repost Incorrect Valuation Entries (Weekly)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-15 12:59:34.392491",
|
"modified": "2026-07-01 14:41:51.499245",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reposting Settings",
|
"name": "Stock Reposting Settings",
|
||||||
|
|||||||
@@ -4,7 +4,16 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
|
from frappe.utils import (
|
||||||
|
add_to_date,
|
||||||
|
get_datetime,
|
||||||
|
get_link_to_form,
|
||||||
|
get_time_str,
|
||||||
|
getdate,
|
||||||
|
time_diff_in_hours,
|
||||||
|
today,
|
||||||
|
)
|
||||||
|
from frappe.utils.user import get_users_with_role
|
||||||
|
|
||||||
|
|
||||||
class StockRepostingSettings(Document):
|
class StockRepostingSettings(Document):
|
||||||
@@ -27,6 +36,7 @@ class StockRepostingSettings(Document):
|
|||||||
]
|
]
|
||||||
no_of_parallel_reposting: DF.Int
|
no_of_parallel_reposting: DF.Int
|
||||||
notify_reposting_error_to_role: DF.Link | None
|
notify_reposting_error_to_role: DF.Link | None
|
||||||
|
repost_incorrect_valuation_entries: DF.Check
|
||||||
start_time: DF.Time | None
|
start_time: DF.Time | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -117,3 +127,205 @@ def create_repost_item_valuation(item_code, warehouse, posting_date):
|
|||||||
"status": "Queued",
|
"status": "Queued",
|
||||||
}
|
}
|
||||||
).submit()
|
).submit()
|
||||||
|
|
||||||
|
|
||||||
|
def repost_incorrect_valuation_entries():
|
||||||
|
"""Weekly scheduler entry point.
|
||||||
|
|
||||||
|
When `repost_incorrect_valuation_entries` is enabled in Stock Reposting Settings, scan each
|
||||||
|
company's Stock Ledger Variance and Stock and Account Value Comparison reports for incorrect stock
|
||||||
|
valuation in the current financial year and auto-create reposts to correct them. Journal Entries are
|
||||||
|
never reposted, and warehouses pointing at a non-'Stock' account are reported to System Managers
|
||||||
|
instead. Disabled by default; does nothing unless explicitly turned on."""
|
||||||
|
if not frappe.db.get_single_value("Stock Reposting Settings", "repost_incorrect_valuation_entries"):
|
||||||
|
return
|
||||||
|
|
||||||
|
for company in frappe.get_all("Company", pluck="name"):
|
||||||
|
# The Stock Ledger Variance scan runs the invariant check for every item-warehouse, so process
|
||||||
|
# each company as its own long-running background job rather than blocking the weekly scheduler.
|
||||||
|
frappe.enqueue(
|
||||||
|
repost_incorrect_valuation_entries_for_company,
|
||||||
|
queue="long",
|
||||||
|
job_id=f"repost_incorrect_valuation::{company}",
|
||||||
|
deduplicate=True,
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def repost_incorrect_valuation_entries_for_company(company):
|
||||||
|
"""Detect and repost incorrect stock valuation for a single company, limited to the current
|
||||||
|
financial year, using two reports:
|
||||||
|
|
||||||
|
1. Stock Ledger Variance - item-warehouses whose ledger valuation is internally inconsistent
|
||||||
|
(typically a wrong previous-SLE pick). Fixed with an Item & Warehouse repost.
|
||||||
|
2. Stock and Account Value Comparison - vouchers whose stock value does not match the accounting
|
||||||
|
ledger. Reposted via the report's own logic (Journal Entries are excluded - ERPNext does not
|
||||||
|
repost them). If a voucher's warehouse points at an account that is not of type 'Stock',
|
||||||
|
reposting can never clear the difference, so System Managers are notified instead."""
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
|
fy_start_date = get_fiscal_year(today(), company=company)[1]
|
||||||
|
|
||||||
|
_repost_stock_ledger_variance(company, fy_start_date)
|
||||||
|
_repost_stock_account_value_comparison(company, fy_start_date)
|
||||||
|
|
||||||
|
|
||||||
|
def _repost_stock_ledger_variance(company, fy_start_date):
|
||||||
|
from erpnext.stock.report.stock_ledger_variance.stock_ledger_variance import (
|
||||||
|
get_data as get_stock_ledger_variance,
|
||||||
|
)
|
||||||
|
|
||||||
|
created = []
|
||||||
|
for row in get_stock_ledger_variance({"company": company}) or []:
|
||||||
|
row = frappe._dict(row)
|
||||||
|
|
||||||
|
# Only correct issues that originate in the current financial year.
|
||||||
|
if not row.posting_date or getdate(row.posting_date) < getdate(fy_start_date):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Avoid piling up duplicate reposts week over week for the same item-warehouse.
|
||||||
|
if has_pending_valuation_repost(company, row.item_code, row.warehouse):
|
||||||
|
continue
|
||||||
|
|
||||||
|
create_repost_item_valuation(row.item_code, row.warehouse, row.posting_date)
|
||||||
|
created.append(row)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
frappe.logger("stock_reposting").info(
|
||||||
|
f"Auto-reposted {len(created)} incorrect-valuation item-warehouse(s) for {company}: "
|
||||||
|
+ ", ".join(f"{d.item_code} @ {d.warehouse} from {d.posting_date}" for d in created)
|
||||||
|
)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
def _repost_stock_account_value_comparison(company, fy_start_date):
|
||||||
|
import erpnext
|
||||||
|
|
||||||
|
# Stock vs accounting values only exist under perpetual inventory.
|
||||||
|
if not erpnext.is_perpetual_inventory_enabled(company):
|
||||||
|
return
|
||||||
|
|
||||||
|
from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import (
|
||||||
|
create_reposting_entries,
|
||||||
|
)
|
||||||
|
from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import (
|
||||||
|
get_data as get_value_comparison,
|
||||||
|
)
|
||||||
|
|
||||||
|
to_repost = []
|
||||||
|
misconfigured = [] # (voucher_type, voucher_no, warehouse, account)
|
||||||
|
|
||||||
|
# Scope the report's DB scan to the current financial year (see get_data) instead of loading every
|
||||||
|
# voucher ever posted and filtering in Python afterwards.
|
||||||
|
comparison_filters = frappe._dict(company=company, from_date=fy_start_date, as_on_date=today())
|
||||||
|
for row in get_value_comparison(comparison_filters) or []:
|
||||||
|
row = frappe._dict(row)
|
||||||
|
|
||||||
|
# ERPNext does not repost Journal Entries (GL-only postings have no stock ledger to repost).
|
||||||
|
if row.voucher_type == "Journal Entry":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only correct issues that originate in the current financial year.
|
||||||
|
if not row.posting_date or getdate(row.posting_date) < getdate(fy_start_date):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If a warehouse on this voucher is mapped to an account that is not of type 'Stock', reposting
|
||||||
|
# can never reconcile stock vs accounting value - flag it for a human instead of reposting.
|
||||||
|
# Only flag accounts with a concrete, non-'Stock' type. An unset/blank account_type is treated as
|
||||||
|
# "unknown" - reposting may well reconcile it - so we don't skip the voucher or email a false alarm.
|
||||||
|
wrong_accounts = [
|
||||||
|
(warehouse, account)
|
||||||
|
for warehouse, account, account_type in get_voucher_warehouse_accounts(row.voucher_no, company)
|
||||||
|
if account_type and account_type != "Stock"
|
||||||
|
]
|
||||||
|
if wrong_accounts:
|
||||||
|
misconfigured.extend(
|
||||||
|
(row.voucher_type, row.voucher_no, warehouse, account)
|
||||||
|
for warehouse, account in wrong_accounts
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
to_repost.append(row)
|
||||||
|
|
||||||
|
if to_repost:
|
||||||
|
# create_reposting_entries reposts Purchase Receipt/Invoice transaction-wise and everything else
|
||||||
|
# item-warehouse-wise, and de-duplicates against existing reposts.
|
||||||
|
create_reposting_entries(to_repost, company)
|
||||||
|
frappe.logger("stock_reposting").info(
|
||||||
|
f"Auto-reposted {len(to_repost)} stock/account value mismatch voucher(s) for {company}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if misconfigured:
|
||||||
|
notify_incorrect_stock_account(company, misconfigured)
|
||||||
|
|
||||||
|
|
||||||
|
def get_voucher_warehouse_accounts(voucher_no, company):
|
||||||
|
"""Return (warehouse, account, account_type) for each distinct warehouse the voucher posted stock
|
||||||
|
into, so the caller can verify the account is a 'Stock' asset account."""
|
||||||
|
warehouses = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_no": voucher_no, "is_cancelled": 0},
|
||||||
|
pluck="warehouse",
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for warehouse in {w for w in warehouses if w}:
|
||||||
|
account = frappe.get_cached_value("Warehouse", warehouse, "account") or frappe.get_cached_value(
|
||||||
|
"Company", company, "default_inventory_account"
|
||||||
|
)
|
||||||
|
account_type = frappe.get_cached_value("Account", account, "account_type") if account else None
|
||||||
|
rows.append((warehouse, account, account_type))
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def notify_incorrect_stock_account(company, misconfigured):
|
||||||
|
"""Email System Managers about warehouse accounts that are not of type 'Stock', which keep stock
|
||||||
|
and accounting values from reconciling even after reposting."""
|
||||||
|
recipients = get_users_with_role("System Manager")
|
||||||
|
if not recipients:
|
||||||
|
return
|
||||||
|
|
||||||
|
items = "".join(
|
||||||
|
"<li>{} {} → {}: {}</li>".format(
|
||||||
|
voucher_type,
|
||||||
|
get_link_to_form(voucher_type, voucher_no),
|
||||||
|
warehouse,
|
||||||
|
account or _("No account set"),
|
||||||
|
)
|
||||||
|
for voucher_type, voucher_no, warehouse, account in misconfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _("Incorrect Stock Asset Account in {0}").format(company)
|
||||||
|
message = (
|
||||||
|
_("Stock and accounting values could not be reconciled by reposting for {0}.").format(
|
||||||
|
frappe.bold(company)
|
||||||
|
)
|
||||||
|
+ "<br><br>"
|
||||||
|
+ _(
|
||||||
|
"The warehouse account(s) below are not of type 'Stock'. Please set a correct Stock asset account on the warehouse (Account Type must be 'Stock'):"
|
||||||
|
)
|
||||||
|
+ f"<ul>{items}</ul>"
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
def has_pending_valuation_repost(company, item_code, warehouse):
|
||||||
|
"""True if an Item & Warehouse repost for this item-warehouse is already queued or running, so the
|
||||||
|
weekly job does not stack duplicate reposts."""
|
||||||
|
return bool(
|
||||||
|
frappe.db.exists(
|
||||||
|
"Repost Item Valuation",
|
||||||
|
{
|
||||||
|
"company": company,
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"based_on": "Item and Warehouse",
|
||||||
|
"status": ("in", ["Queued", "In Progress"]),
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,137 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import add_days, getdate, today
|
||||||
|
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import get_recipients
|
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import get_recipients
|
||||||
|
from erpnext.stock.doctype.stock_reposting_settings import stock_reposting_settings as srs
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
TEST_COMPANY = "_Test Company"
|
||||||
|
TEST_WAREHOUSE = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
|
||||||
class TestStockRepostingSettings(ERPNextTestSuite):
|
class TestStockRepostingSettings(ERPNextTestSuite):
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "repost_incorrect_valuation_entries", 0)
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_auto_repost_disabled_does_nothing(self):
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "repost_incorrect_valuation_entries", 0)
|
||||||
|
with patch("frappe.enqueue") as enqueue:
|
||||||
|
srs.repost_incorrect_valuation_entries()
|
||||||
|
enqueue.assert_not_called()
|
||||||
|
|
||||||
|
def test_auto_repost_enabled_enqueues_per_company(self):
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "repost_incorrect_valuation_entries", 1)
|
||||||
|
with patch("frappe.enqueue") as enqueue:
|
||||||
|
srs.repost_incorrect_valuation_entries()
|
||||||
|
self.assertTrue(enqueue.called)
|
||||||
|
# one job per company
|
||||||
|
self.assertEqual(enqueue.call_count, frappe.db.count("Company"))
|
||||||
|
|
||||||
|
def test_reposts_only_current_financial_year_entries(self):
|
||||||
|
item = make_item().name
|
||||||
|
fy_start_date = get_fiscal_year(today(), company=TEST_COMPANY)[1]
|
||||||
|
|
||||||
|
current_fy_row = {"item_code": item, "warehouse": TEST_WAREHOUSE, "posting_date": today()}
|
||||||
|
prior_fy_row = {
|
||||||
|
"item_code": item,
|
||||||
|
"warehouse": TEST_WAREHOUSE,
|
||||||
|
"posting_date": add_days(fy_start_date, -1),
|
||||||
|
}
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
variance_path = "erpnext.stock.report.stock_ledger_variance.stock_ledger_variance.get_data"
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
srs, "create_repost_item_valuation", side_effect=lambda i, w, d: calls.append((i, w, str(d)))
|
||||||
|
),
|
||||||
|
patch(variance_path, return_value=[current_fy_row, prior_fy_row]),
|
||||||
|
):
|
||||||
|
srs.repost_incorrect_valuation_entries_for_company(TEST_COMPANY)
|
||||||
|
|
||||||
|
# Only the current-FY entry is reposted; the prior-FY one is ignored.
|
||||||
|
self.assertEqual(calls, [(item, TEST_WAREHOUSE, str(today()))])
|
||||||
|
|
||||||
|
def test_skips_when_repost_already_pending(self):
|
||||||
|
item = make_item().name
|
||||||
|
current_fy_row = {"item_code": item, "warehouse": TEST_WAREHOUSE, "posting_date": today()}
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
variance_path = "erpnext.stock.report.stock_ledger_variance.stock_ledger_variance.get_data"
|
||||||
|
with (
|
||||||
|
patch.object(srs, "has_pending_valuation_repost", return_value=True),
|
||||||
|
patch.object(
|
||||||
|
srs, "create_repost_item_valuation", side_effect=lambda i, w, d: calls.append((i, w, str(d)))
|
||||||
|
),
|
||||||
|
patch(variance_path, return_value=[current_fy_row]),
|
||||||
|
):
|
||||||
|
srs.repost_incorrect_valuation_entries_for_company(TEST_COMPANY)
|
||||||
|
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
|
||||||
|
def test_value_comparison_excludes_je_and_flags_wrong_account(self):
|
||||||
|
fy_start = getdate(today())
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
# correct stock account, current FY -> reposted
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": "PR-OK", "posting_date": today()},
|
||||||
|
# Journal Entry -> never reposted
|
||||||
|
{"voucher_type": "Journal Entry", "voucher_no": "JE-1", "posting_date": today()},
|
||||||
|
# warehouse account not of type "Stock" -> notify, not reposted
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": "PR-BADACC", "posting_date": today()},
|
||||||
|
# warehouse account with an unset account_type -> unknown, repost (not a false alarm)
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": "PR-NOTYPE", "posting_date": today()},
|
||||||
|
# prior financial year -> ignored
|
||||||
|
{
|
||||||
|
"voucher_type": "Purchase Receipt",
|
||||||
|
"voucher_no": "PR-OLD",
|
||||||
|
"posting_date": add_days(fy_start, -1),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
accounts = {
|
||||||
|
"PR-OK": [("WH-A", "Stock A - _TC", "Stock")],
|
||||||
|
"PR-BADACC": [("WH-B", "Debtors - _TC", "Receivable")],
|
||||||
|
"PR-NOTYPE": [("WH-C", "Unclassified - _TC", None)],
|
||||||
|
"PR-OLD": [("WH-A", "Stock A - _TC", "Stock")],
|
||||||
|
}
|
||||||
|
|
||||||
|
reposted = {}
|
||||||
|
sent = []
|
||||||
|
comparison = (
|
||||||
|
"erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison"
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch("erpnext.is_perpetual_inventory_enabled", return_value=True),
|
||||||
|
patch(f"{comparison}.get_data", return_value=rows),
|
||||||
|
patch(
|
||||||
|
f"{comparison}.create_reposting_entries",
|
||||||
|
side_effect=lambda r, c: reposted.update(rows=r, company=c),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
srs, "get_voucher_warehouse_accounts", side_effect=lambda vno, c: accounts.get(vno, [])
|
||||||
|
),
|
||||||
|
patch.object(srs, "get_users_with_role", return_value=["sysmgr@test.com"]),
|
||||||
|
patch("frappe.sendmail", side_effect=lambda **kw: sent.append(kw)),
|
||||||
|
):
|
||||||
|
srs._repost_stock_account_value_comparison(TEST_COMPANY, fy_start)
|
||||||
|
|
||||||
|
# Current-FY, non-Journal-Entry vouchers are reposted: the correct-account one and the one whose
|
||||||
|
# account_type is unset (unknown is treated as "proceed", not "wrong account").
|
||||||
|
self.assertEqual([r["voucher_no"] for r in reposted["rows"]], ["PR-OK", "PR-NOTYPE"])
|
||||||
|
# Only the concrete wrong-account voucher triggers a System Manager notification.
|
||||||
|
self.assertEqual(len(sent), 1)
|
||||||
|
self.assertIn("PR-BADACC", sent[0]["message"])
|
||||||
|
self.assertNotIn("PR-NOTYPE", sent[0]["message"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockRepostingSettingsNotification(ERPNextTestSuite):
|
||||||
def test_notify_reposting_error_to_role(self):
|
def test_notify_reposting_error_to_role(self):
|
||||||
role = "Notify Reposting Role"
|
role = "Notify Reposting Role"
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ def get_data(report_filters):
|
|||||||
"posting_date": ("<=", report_filters.as_on_date),
|
"posting_date": ("<=", report_filters.as_on_date),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional lower bound: lets callers (e.g. the weekly auto-repost job) scope the scan to the current
|
||||||
|
# fiscal year in the query itself instead of loading every voucher ever posted and filtering later.
|
||||||
|
if report_filters.get("from_date"):
|
||||||
|
filters["posting_date"] = ("between", [report_filters.from_date, report_filters.as_on_date])
|
||||||
|
|
||||||
get_currency_precision() or 2
|
get_currency_precision() or 2
|
||||||
stock_ledger_entries = get_stock_ledger_data(report_filters, filters)
|
stock_ledger_entries = get_stock_ledger_data(report_filters, filters)
|
||||||
voucher_wise_gl_data = get_gl_data(report_filters, filters)
|
voucher_wise_gl_data = get_gl_data(report_filters, filters)
|
||||||
@@ -240,19 +245,26 @@ def repost_based_on_transaction(rows, company=None, entries=None):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
duplicate_vouchers.add(voucher_key)
|
duplicate_vouchers.add(voucher_key)
|
||||||
doc = frappe.get_doc(
|
# Isolate each submit in a savepoint: an already-queued repost raises DuplicateEntryError, and on
|
||||||
{
|
# PostgreSQL a failed insert aborts the whole transaction, killing the rest of the loop (and the
|
||||||
"doctype": "Repost Item Valuation",
|
# silent weekly job). Rolling back to the savepoint keeps prior/next reposts intact.
|
||||||
"based_on": "Transaction",
|
frappe.db.savepoint("repost_based_on_transaction")
|
||||||
"status": "Queued",
|
try:
|
||||||
"voucher_type": row.get("voucher_type"),
|
doc = frappe.get_doc(
|
||||||
"voucher_no": row.get("voucher_no"),
|
{
|
||||||
"posting_date": row.get("posting_date"),
|
"doctype": "Repost Item Valuation",
|
||||||
"posting_time": row.get("posting_time"),
|
"based_on": "Transaction",
|
||||||
"company": company,
|
"status": "Queued",
|
||||||
"allow_nagative_stock": 1,
|
"voucher_type": row.get("voucher_type"),
|
||||||
"recalculate_valuation_rate": 1,
|
"voucher_no": row.get("voucher_no"),
|
||||||
}
|
"posting_date": row.get("posting_date"),
|
||||||
).submit()
|
"posting_time": row.get("posting_time"),
|
||||||
|
"company": company,
|
||||||
|
"allow_nagative_stock": 1,
|
||||||
|
"recalculate_valuation_rate": 1,
|
||||||
|
}
|
||||||
|
).submit()
|
||||||
|
|
||||||
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
|
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
frappe.db.rollback(save_point="repost_based_on_transaction")
|
||||||
|
|||||||
Reference in New Issue
Block a user