mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 06:31:48 +00:00
feat: stock validations in Period Closing Voucher and snapshot-seeded batch valuation (backport #57811) (#57816)
* feat: validate stock value and stock closing entry before period closing (cherry picked from commit20450bd4ec) * fix: do not accept scoped stock closing entries as period closing prerequisite (cherry picked from commit359a347be2) * feat: seed batch valuation from stock closing balance and freeze closed-period stock (cherry picked from commit49a127d59c)
This commit is contained in:
@@ -6,9 +6,10 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, flt, formatdate, getdate
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.utils import add_days, flt, fmt_money, formatdate, get_link_to_form, getdate
|
||||
|
||||
from erpnext import is_perpetual_inventory_enabled
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
@@ -18,6 +19,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
from erpnext.accounts.general_ledger import check_freezing_date, is_immutable_ledger_enabled
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import apply_unscoped_filters
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
|
||||
class PeriodClosingVoucher(AccountsController):
|
||||
@@ -139,6 +142,121 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if account_currency != company_currency:
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def before_submit(self):
|
||||
if not self.has_stock_transactions():
|
||||
return
|
||||
|
||||
self.validate_stock_accounts_balance()
|
||||
self.validate_stock_closing_entry()
|
||||
|
||||
def has_stock_transactions(self):
|
||||
if not is_perpetual_inventory_enabled(self.company):
|
||||
return False
|
||||
|
||||
return bool(
|
||||
frappe.db.exists(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
"posting_date": ("<=", self.period_end_date),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def validate_stock_accounts_balance(self):
|
||||
precision = frappe.get_precision("GL Entry", "debit")
|
||||
account_balance = flt(self.get_stock_accounts_balance(), precision)
|
||||
stock_value = flt(
|
||||
get_stock_value_on(posting_date=self.period_end_date, company=self.company), precision
|
||||
)
|
||||
|
||||
if account_balance == stock_value:
|
||||
return
|
||||
|
||||
currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
frappe.throw(
|
||||
_(
|
||||
"The closing balance {0} of the Stock Asset accounts does not match the closing value {1} of the Stock Balance report as on {2}. Resolve the difference using the Stock Ledger Variance report before closing the period."
|
||||
).format(
|
||||
frappe.bold(fmt_money(account_balance, currency=currency)),
|
||||
frappe.bold(fmt_money(stock_value, currency=currency)),
|
||||
frappe.bold(formatdate(self.period_end_date)),
|
||||
),
|
||||
title=_("Stock Value Mismatch"),
|
||||
)
|
||||
|
||||
def get_stock_accounts_balance(self):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
account = frappe.qb.DocType("Account")
|
||||
|
||||
stock_accounts = (
|
||||
frappe.qb.from_(account)
|
||||
.select(account.name)
|
||||
.where(
|
||||
(account.account_type == "Stock")
|
||||
& (account.company == self.company)
|
||||
& (account.is_group == 0)
|
||||
)
|
||||
)
|
||||
|
||||
balance = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.debit - gle.credit))
|
||||
.where(
|
||||
(gle.company == self.company)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.posting_date <= self.period_end_date)
|
||||
& gle.account.isin(stock_accounts)
|
||||
)
|
||||
).run()
|
||||
|
||||
return flt(balance[0][0]) if balance else 0.0
|
||||
|
||||
def validate_stock_closing_entry(self):
|
||||
closing_entry = frappe.db.get_value(
|
||||
"Stock Closing Entry",
|
||||
apply_unscoped_filters(
|
||||
{"company": self.company, "to_date": self.period_end_date, "docstatus": 1}
|
||||
),
|
||||
["name", "status", "modified"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not closing_entry:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Create a Stock Closing Entry for the entire company with To Date as {0} before submitting the Period Closing Voucher."
|
||||
).format(frappe.bold(formatdate(self.period_end_date))),
|
||||
title=_("Stock Closing Entry Required"),
|
||||
)
|
||||
|
||||
if closing_entry.status != "Completed":
|
||||
frappe.throw(
|
||||
_(
|
||||
"The Stock Closing Entry for {0} is not completed yet. Wait for it to complete before submitting the Period Closing Voucher."
|
||||
).format(frappe.bold(formatdate(self.period_end_date))),
|
||||
title=_("Stock Closing Entry In Progress"),
|
||||
)
|
||||
|
||||
self.validate_stock_closing_entry_is_fresh(closing_entry)
|
||||
|
||||
def validate_stock_closing_entry_is_fresh(self, closing_entry):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
last_change = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(Max(sle.modified))
|
||||
.where((sle.company == self.company) & (sle.posting_date <= self.period_end_date))
|
||||
).run()
|
||||
|
||||
if last_change and last_change[0][0] and last_change[0][0] > closing_entry.modified:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Stock transactions were created or modified after the Stock Closing Entry {0} was generated. Regenerate it before submitting the Period Closing Voucher."
|
||||
).format(get_link_to_form("Stock Closing Entry", closing_entry.name)),
|
||||
title=_("Stock Closing Entry Outdated"),
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
@@ -307,6 +307,218 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
repost_doc.posting_date = today()
|
||||
repost_doc.save()
|
||||
|
||||
def test_stock_validations_before_period_closing(self):
|
||||
from unittest.mock import patch
|
||||
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
create_custom_fields(
|
||||
{
|
||||
"Stock Closing Entry": [
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"label": "Warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
item = make_item("Test PCV Stock Item", {"is_stock_item": 1})
|
||||
se = make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Create a Stock Closing Entry", pcv.submit)
|
||||
|
||||
sce = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Closing Entry",
|
||||
"company": "Test PCV Company",
|
||||
"from_date": pcv.period_start_date,
|
||||
"to_date": pcv.period_end_date,
|
||||
"warehouse": "Stores - TPC",
|
||||
}
|
||||
).insert()
|
||||
|
||||
with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"):
|
||||
sce.submit()
|
||||
|
||||
sce.db_set("status", "Completed")
|
||||
|
||||
pcv.reload()
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Create a Stock Closing Entry", pcv.submit)
|
||||
|
||||
frappe.db.set_value("Stock Closing Entry", sce.name, {"warehouse": None, "status": "In Progress"})
|
||||
|
||||
pcv.reload()
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is not completed yet", pcv.submit)
|
||||
|
||||
sce.create_stock_closing_balance_entries()
|
||||
sce.db_set("status", "Completed")
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": se.name},
|
||||
["name", "stock_value_difference"],
|
||||
as_dict=1,
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry", sle.name, "stock_value_difference", sle.stock_value_difference + 100
|
||||
)
|
||||
|
||||
pcv.reload()
|
||||
self.assertRaisesRegex(frappe.ValidationError, "does not match", pcv.submit)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry", sle.name, "stock_value_difference", sle.stock_value_difference
|
||||
)
|
||||
|
||||
pcv.reload()
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Regenerate", pcv.submit)
|
||||
|
||||
self.rebuild_stock_closing_balance(sce)
|
||||
pcv.reload()
|
||||
pcv.submit()
|
||||
self.assertEqual(pcv.docstatus, 1)
|
||||
|
||||
def test_batch_valuation_seeded_from_stock_closing_after_period_closing(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
item = make_item(
|
||||
"Test PCV Batch Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TPCVB.####",
|
||||
},
|
||||
)
|
||||
se1 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
rate=200,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-06-15",
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
|
||||
sce = self.make_completed_stock_closing_entry(pcv.period_start_date, pcv.period_end_date)
|
||||
pcv.reload()
|
||||
pcv.submit()
|
||||
|
||||
outward = make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=5,
|
||||
from_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2022-04-01",
|
||||
batch_no=batch_no,
|
||||
)
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": outward.name, "is_cancelled": 0},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(flt(stock_value_difference, 2), -750.0)
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"frozen",
|
||||
make_stock_entry,
|
||||
item_code=item.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-05-01",
|
||||
)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "frozen", se1.cancel)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "closed accounting period", sce.cancel)
|
||||
|
||||
def test_period_closing_blocks_stale_stock_closing_entry(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
item = make_item("Test PCV Stock Item", {"is_stock_item": 1})
|
||||
make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
|
||||
sce = self.make_completed_stock_closing_entry(pcv.period_start_date, pcv.period_end_date)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item.name,
|
||||
qty=5,
|
||||
rate=100,
|
||||
to_warehouse="Stores - TPC",
|
||||
company="Test PCV Company",
|
||||
posting_date="2021-05-01",
|
||||
)
|
||||
|
||||
pcv.reload()
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Regenerate", pcv.submit)
|
||||
|
||||
self.rebuild_stock_closing_balance(sce)
|
||||
pcv.reload()
|
||||
pcv.submit()
|
||||
self.assertEqual(pcv.docstatus, 1)
|
||||
|
||||
def make_completed_stock_closing_entry(self, from_date, to_date):
|
||||
from unittest.mock import patch
|
||||
|
||||
sce = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Closing Entry",
|
||||
"company": "Test PCV Company",
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
}
|
||||
).insert()
|
||||
|
||||
with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"):
|
||||
sce.submit()
|
||||
|
||||
sce.create_stock_closing_balance_entries()
|
||||
sce.db_set("status", "Completed")
|
||||
return sce
|
||||
|
||||
def rebuild_stock_closing_balance(self, sce):
|
||||
sce.remove_stock_closing()
|
||||
sce.create_stock_closing_balance_entries()
|
||||
sce.db_set("status", "Completed")
|
||||
|
||||
def make_period_closing_voucher(self, posting_date, submit=True):
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
@@ -159,6 +159,9 @@ class DeprecatedBatchNoValuation:
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
if getattr(self, "stock_closing_from_datetime", None):
|
||||
query = query.where(sle.posting_datetime >= self.stock_closing_from_datetime)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
@deprecated(
|
||||
|
||||
@@ -10,9 +10,51 @@ from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, get_date_str, get_link_to_form, nowtime, parse_json
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.caching import request_cache
|
||||
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
|
||||
SCOPE_FIELDS = ("warehouse", "item_code", "item_group", "warehouse_type")
|
||||
|
||||
|
||||
def apply_unscoped_filters(filters):
|
||||
meta = frappe.get_meta("Stock Closing Entry")
|
||||
for fieldname in SCOPE_FIELDS:
|
||||
if meta.has_field(fieldname):
|
||||
filters[fieldname] = ("is", "not set")
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def get_closing_entry_for_closed_period(company):
|
||||
closed_upto = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
|
||||
)
|
||||
if not closed_upto:
|
||||
return None
|
||||
|
||||
return _get_completed_closing_entry(company, str(closed_upto))
|
||||
|
||||
|
||||
@request_cache
|
||||
def _get_completed_closing_entry(company, closed_upto):
|
||||
filters = apply_unscoped_filters(
|
||||
{
|
||||
"company": company,
|
||||
"docstatus": 1,
|
||||
"status": "Completed",
|
||||
"to_date": ("<=", closed_upto),
|
||||
}
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
"Stock Closing Entry",
|
||||
filters,
|
||||
["name", "to_date"],
|
||||
order_by="to_date desc",
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
class StockClosingEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -68,7 +110,7 @@ class StockClosingEntry(Document):
|
||||
)
|
||||
)
|
||||
|
||||
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||
for fieldname in SCOPE_FIELDS:
|
||||
if self.get(fieldname):
|
||||
query = query.where(table[fieldname] == self.get(fieldname))
|
||||
|
||||
@@ -86,14 +128,30 @@ class StockClosingEntry(Document):
|
||||
self.enqueue_job()
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_closed_period_lock()
|
||||
self.set_status(save=True)
|
||||
self.remove_stock_closing()
|
||||
|
||||
def validate_closed_period_lock(self):
|
||||
pcv = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
{"company": self.company, "docstatus": 1, "period_end_date": (">=", self.to_date)},
|
||||
"name",
|
||||
)
|
||||
|
||||
if pcv:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Stock Closing Entry {0} belongs to a closed accounting period. Cancel the Period Closing Voucher {1} first."
|
||||
).format(self.name, get_link_to_form("Period Closing Voucher", pcv)),
|
||||
title=_("Closed Period"),
|
||||
)
|
||||
|
||||
def remove_stock_closing(self):
|
||||
table = frappe.qb.DocType("Stock Closing Balance")
|
||||
frappe.qb.from_(table).delete().where(table.stock_closing_entry == self.name).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def enqueue_job(self):
|
||||
self.db_set("status", "In Progress")
|
||||
enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500)
|
||||
@@ -103,8 +161,9 @@ class StockClosingEntry(Document):
|
||||
).format(self.name)
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def regenerate_closing_balance(self):
|
||||
self.validate_closed_period_lock()
|
||||
self.remove_stock_closing()
|
||||
self.enqueue_job()
|
||||
|
||||
|
||||
@@ -820,13 +820,14 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
|
||||
)
|
||||
else:
|
||||
entries = self.get_batch_stock_before_date()
|
||||
self.stock_value_change = 0.0
|
||||
self.batch_avg_rate = defaultdict(float)
|
||||
self.available_qty = defaultdict(float)
|
||||
self.stock_value_differece = defaultdict(float)
|
||||
|
||||
for ledger in entries:
|
||||
self.seed_from_stock_closing_balance()
|
||||
|
||||
for ledger in self.get_batch_stock_before_date():
|
||||
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
|
||||
self.available_qty[ledger.batch_no] += flt(ledger.qty)
|
||||
|
||||
@@ -834,6 +835,52 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
self.calculate_avg_rate_for_non_batchwise_valuation()
|
||||
self.set_stock_value_difference()
|
||||
|
||||
def seed_from_stock_closing_balance(self):
|
||||
self.stock_closing_from_datetime = None
|
||||
closing_entry = self.get_closing_entry_for_seeding()
|
||||
if not closing_entry:
|
||||
return
|
||||
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
self.stock_closing_from_datetime = get_combine_datetime(
|
||||
add_days(closing_entry.to_date, 1), "00:00:00"
|
||||
)
|
||||
|
||||
for row in self.get_stock_closing_balance_entries(closing_entry.name):
|
||||
self.stock_value_differece[row.batch_no] += flt(row.stock_value_difference)
|
||||
self.available_qty[row.batch_no] += flt(row.actual_qty)
|
||||
|
||||
def get_closing_entry_for_seeding(self):
|
||||
from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import (
|
||||
get_closing_entry_for_closed_period,
|
||||
)
|
||||
|
||||
if not self.batchwise_valuation_batches or not self.sle.posting_date:
|
||||
return None
|
||||
|
||||
company = self.sle.company or frappe.get_cached_value("Warehouse", self.sle.warehouse, "company")
|
||||
closing_entry = get_closing_entry_for_closed_period(company)
|
||||
if not closing_entry or getdate(self.sle.posting_date) <= getdate(closing_entry.to_date):
|
||||
return None
|
||||
|
||||
return closing_entry
|
||||
|
||||
def get_stock_closing_balance_entries(self, closing_entry):
|
||||
table = frappe.qb.DocType("Stock Closing Balance")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.batch_no, table.actual_qty, table.stock_value_difference)
|
||||
.where(
|
||||
(table.stock_closing_entry == closing_entry)
|
||||
& (table.item_code == self.sle.item_code)
|
||||
& (table.warehouse == self.sle.warehouse)
|
||||
& table.batch_no.isin(self.batchwise_valuation_batches)
|
||||
& (table.inventory_dimension_key.isnull() | (table.inventory_dimension_key == ""))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
def get_batch_stock_before_date(self) -> list[dict]:
|
||||
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
|
||||
if not self.batchwise_valuation_batches:
|
||||
@@ -909,6 +956,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
if timestamp_condition:
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
if self.stock_closing_from_datetime:
|
||||
query = query.where(child.posting_datetime >= self.stock_closing_from_datetime)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def prepare_batches(self):
|
||||
|
||||
@@ -56,6 +56,32 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_stock_frozen_by_closing_entry(sl_entries):
|
||||
from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import (
|
||||
get_closing_entry_for_closed_period,
|
||||
)
|
||||
|
||||
company = sl_entries[0].get("company")
|
||||
if not company:
|
||||
company = frappe.get_cached_value("Warehouse", sl_entries[0].get("warehouse"), "company")
|
||||
|
||||
closing_entry = get_closing_entry_for_closed_period(company)
|
||||
if not closing_entry:
|
||||
return
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.get("posting_date") and getdate(sle.get("posting_date")) <= getdate(closing_entry.to_date):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Stock transactions dated on or before {0} are frozen because the period is closed and the Stock Closing Entry {1} has been generated. To make changes, cancel the Period Closing Voucher first."
|
||||
).format(
|
||||
frappe.bold(format_date(closing_entry.to_date)),
|
||||
get_link_to_form("Stock Closing Entry", closing_entry.name),
|
||||
),
|
||||
title=_("Stock Frozen"),
|
||||
)
|
||||
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
"""Create SL entries from SL entry dicts
|
||||
|
||||
@@ -70,6 +96,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
|
||||
if sl_entries:
|
||||
validate_stock_frozen_by_closing_entry(sl_entries)
|
||||
|
||||
cancelled = sl_entries[0].get("is_cancelled")
|
||||
if cancelled:
|
||||
validate_cancellation(sl_entries)
|
||||
|
||||
Reference in New Issue
Block a user