Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
765018c08e chore(deps): bump socket.io-parser from 4.2.6 to 4.2.7 in /banking
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.6 to 4.2.7.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.6...socket.io-parser@4.2.7)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 05:35:15 +00:00
44 changed files with 51617 additions and 137934 deletions

View File

@@ -3449,9 +3449,9 @@ socket.io-client@4.7.1:
socket.io-parser "~4.2.4"
socket.io-parser@~4.2.4:
version "4.2.6"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.6.tgz#19156bf179af3931abd05260cfb1491822578a6f"
integrity sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==
version "4.2.7"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz#679e51fe24d1c81df90fc5f7efe4a5f432fe99c0"
integrity sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.4.1"

View File

@@ -6,10 +6,8 @@ import copy
import frappe
from frappe import _
from frappe.query_builder.functions import Max, Sum
from frappe.utils import add_days, flt, fmt_money, formatdate, get_link_to_form, getdate
from frappe.utils import add_days, flt, formatdate, getdate
from erpnext import is_perpetual_inventory_enabled
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
@@ -19,8 +17,6 @@ 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):
@@ -145,121 +141,6 @@ 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"):

View File

@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils import flt, today
from frappe.utils import 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
@@ -386,218 +386,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(acb_figures["Cash"][key_for(cc1)], 400)
self.assertEqual(acb_figures["Cash"][key_for(cc2)], 200)
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")

View File

@@ -587,12 +587,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
super.set_dynamic_labels();
this.frm.events.hide_fields(this.frm);
const hide_update_stock = cint(this.frm.doc.is_debit_note) || cint(this.frm.doc.has_subcontracted);
// frm.set_df_property mutates a per-document copy, not the doctype's shared field
// metadata, so this always reflects the original (Customize Form) hidden value.
const hidden_by_customization = cint(
frappe.meta.get_docfield("Sales Invoice", "update_stock")?.hidden
);
this.frm.set_df_property("update_stock", "hidden", hide_update_stock || hidden_by_customization);
this.frm.set_df_property("update_stock", "hidden", hide_update_stock);
}
items_on_form_rendered() {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -120,8 +120,8 @@ class BlanketOrder(Document):
def validate_item_qty(self):
for d in self.items:
if flt(d.qty) <= 0:
frappe.throw(_("Row {0}: Quantity must be greater than zero.").format(d.idx))
if flt(d.qty) < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))
@frappe.whitelist()
@@ -149,11 +149,7 @@ def make_order(source_name: str):
"Blanket Order",
source_name,
{
"Blanket Order": {
"doctype": doctype,
"field_no_map": ["naming_series"],
"postprocess": update_doc,
},
"Blanket Order": {"doctype": doctype, "postprocess": update_doc},
"Blanket Order Item": {
"doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},

View File

@@ -25,7 +25,6 @@ class TestBlanketOrder(ERPNextTestSuite):
so.submit()
self.assertEqual(so.doctype, "Sales Order")
self.assertNotEqual(so.naming_series, bo.naming_series)
self.assertEqual(len(so.get("items")), len(bo.get("items")))
# check the rate, quantity and updation for the ordered quantity
@@ -51,7 +50,6 @@ class TestBlanketOrder(ERPNextTestSuite):
po.submit()
self.assertEqual(po.doctype, "Purchase Order")
self.assertNotEqual(po.naming_series, bo.naming_series)
self.assertEqual(len(po.get("items")), len(bo.get("items")))
# check the rate, quantity and updation for the ordered quantity
@@ -164,26 +162,6 @@ class TestBlanketOrder(ERPNextTestSuite):
bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code)
self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1")
def test_blanket_order_zero_quantity(self):
bo = frappe.new_doc("Blanket Order")
bo.blanket_order_type = "Selling"
bo.company = "_Test Company"
bo.customer = "_Test Customer"
bo.from_date = today()
bo.to_date = add_months(today(), 12)
bo.append(
"items",
{
"item_code": "_Test Item",
"qty": 0,
"rate": 100,
},
)
with self.assertRaises(frappe.ValidationError):
bo.insert()
def make_blanket_order(**args):
args = frappe._dict(args)

View File

@@ -145,9 +145,6 @@ class DeprecatedBatchNoValuation:
if self.sle.name:
conditions &= sle.name != self.sle.name
if getattr(self, "stock_closing_from_datetime", None):
conditions &= sle.posting_datetime >= self.stock_closing_from_datetime
# MariaDB carries a row lock on the grouped query below; on postgres the caller
# (calculate_avg_rate) serializes via a txn-scoped advisory lock on (item, warehouse).
query = (

View File

@@ -4,7 +4,7 @@
import json
import frappe
from frappe.utils import add_days, add_to_date, flt, nowtime, today
from frappe.utils import flt, nowtime, today
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -1637,190 +1637,3 @@ class TestSerialandBatchBundleLogic(ERPNextTestSuite):
self.assertNotIn(bundles[1], bundle_wise_serial_nos)
self.assertEqual(bundle_wise_serial_nos[bundles[0]], [serial_no])
@ERPNextTestSuite.change_settings(
"Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}
)
def test_batchwise_valuation_for_same_posting_datetime_entries(self):
# an inward at a different rate and multiple outward rows with the same
# item and warehouse share the same posting datetime, the tie-breaking
# must include the same-timestamp entries which are already part of the
# ledger and must not let the outward rows count each other
item_code = make_item(
"Test Batchwise Same Posting Datetime Item 1",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBSPD-ITEM1-.#####",
"valuation_method": "FIFO",
},
).name
warehouse = "_Test Warehouse - _TC"
receipt = make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
posting_date=add_days(today(), -5),
posting_time="12:00:00",
)
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
self.assertTrue(frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"))
# same posting datetime as the outward rows below, at a different rate
make_stock_entry(
item_code=item_code,
qty=20,
rate=250,
target=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
posting_time="12:00:00",
)
issue = make_stock_entry(
item_code=item_code,
qty=2,
source=warehouse,
posting_date=add_days(today(), -3),
posting_time="12:00:00",
do_not_save=True,
)
for qty in [3, 4]:
issue.append(
"items",
{
"item_code": item_code,
"s_warehouse": warehouse,
"qty": qty,
"conversion_factor": 1,
},
)
issue.save()
issue.submit()
# (10 * 100 + 20 * 250) / 30 = 200
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=200.0, balance_value=4200.0)
# backdated receipt reposts the same posting datetime cluster
make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
posting_date=add_days(today(), -4),
posting_time="12:00:00",
)
# (20 * 100 + 20 * 250) / 40 = 175
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=175.0, balance_value=5425.0)
@ERPNextTestSuite.change_settings(
"Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}
)
def test_batchwise_valuation_when_bundle_created_before_the_sle(self):
# a bundle can be created (drafted) much before / after its SLE, the
# tie-breaking for the same posting datetime entries must follow the
# SLE creation and not the bundle creation
item_code = make_item(
"Test Batchwise Same Posting Datetime Item 2",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBSPD-ITEM2-.#####",
"valuation_method": "FIFO",
},
).name
warehouse = "_Test Warehouse - _TC"
receipt = make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
posting_date=add_days(today(), -5),
posting_time="12:00:00",
)
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
# inward at a different rate, same posting datetime as the outward below
inward = make_stock_entry(
item_code=item_code,
qty=10,
rate=200,
target=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
posting_time="12:00:00",
)
outward = make_stock_entry(
item_code=item_code,
qty=10,
source=warehouse,
posting_date=add_days(today(), -3),
posting_time="12:00:00",
)
# simulate the inward's bundle drafted after the outward's SLE, the
# bundle creation timeline no longer matches the SLE creation timeline
outward_sle_creation = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": outward.name, "is_cancelled": 0},
"creation",
)
frappe.db.set_value(
"Serial and Batch Bundle",
inward.items[0].serial_and_batch_bundle,
"creation",
add_to_date(outward_sle_creation, minutes=30),
update_modified=False,
)
repost = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Item and Warehouse",
"item_code": item_code,
"warehouse": warehouse,
"posting_date": add_days(today(), -6),
"posting_time": "00:00:00",
"allow_negative_stock": 1,
}
)
repost.submit()
# (10 * 100 + 10 * 200) / 20 = 150, the inward precedes the outward as
# per the SLE creation even though its bundle was created afterwards
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=150.0, balance_value=1500.0)
def assert_batchwise_outgoing_rate(self, item_code, outgoing_rate, balance_value):
sl_entries = frappe.get_all(
"Stock Ledger Entry",
filters={"item_code": item_code, "is_cancelled": 0},
fields=["actual_qty", "stock_value_difference", "stock_value"],
order_by="posting_datetime, creation",
)
for sle in sl_entries:
if sle.actual_qty > 0:
continue
self.assertEqual(flt(sle.stock_value_difference, 2), flt(sle.actual_qty * outgoing_rate, 2))
self.assertEqual(flt(sl_entries[-1].stock_value, 2), flt(balance_value, 2))

View File

@@ -9,51 +9,9 @@ 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
@@ -108,7 +66,7 @@ class StockClosingEntry(Document):
)
)
for fieldname in SCOPE_FIELDS:
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.get(fieldname):
query = query.where(table[fieldname] == self.get(fieldname))
@@ -126,30 +84,14 @@ 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(methods=["POST"])
@frappe.whitelist()
def enqueue_job(self):
self.db_set("status", "In Progress")
enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500)
@@ -159,9 +101,8 @@ class StockClosingEntry(Document):
).format(self.name)
)
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def regenerate_closing_balance(self):
self.validate_closed_period_lock()
self.remove_stock_closing()
self.enqueue_job()

View File

@@ -6,7 +6,6 @@ from frappe.model.naming import NamingSeries, parse_naming_series
from frappe.query_builder.functions import Max, Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now
from pypika import Order
from pypika.terms import ExistsCriterion
from erpnext.stock.deprecated_serial_batch import (
DeprecatedBatchNoValuation,
@@ -831,14 +830,13 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
("batch-valuation", self.sle.item_code, self.sle.warehouse)
)
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)
self.seed_from_stock_closing_balance()
for ledger in self.get_batch_stock_before_date():
for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
self.available_qty[ledger.batch_no] += flt(ledger.qty)
@@ -846,52 +844,6 @@ 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:
@@ -899,45 +851,14 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
child = frappe.qb.DocType("Serial and Batch Entry")
sle_creation = self.sle.creation if self.sle.get("name") else None
if not self.sle.get("name") and self.sle.get("serial_and_batch_bundle"):
sle_creation = frappe.db.get_value(
"Stock Ledger Entry",
{"serial_and_batch_bundle": self.sle.serial_and_batch_bundle, "is_cancelled": 0},
"creation",
)
timestamp_condition = ""
if self.sle.posting_datetime:
timestamp_condition = child.posting_datetime < self.sle.posting_datetime
sle_table = frappe.qb.DocType("Stock Ledger Entry")
if sle_creation:
# bundle creation and SLE creation are different timelines (a
# bundle can be created much before its SLE), so break the tie
# using the creation of the bundle's own SLE
tie_condition = ExistsCriterion(
frappe.qb.from_(sle_table)
.select(sle_table.name)
.where(
(sle_table.serial_and_batch_bundle == child.parent)
& (sle_table.is_cancelled == 0)
& (sle_table.creation < sle_creation)
)
if self.sle.creation:
timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & (
child.creation < self.sle.creation
)
else:
# the current entry is not yet in the ledger and will get the
# latest creation, so the same-timestamp entries which are
# already in the ledger precede it
tie_condition = ExistsCriterion(
frappe.qb.from_(sle_table)
.select(sle_table.name)
.where(
(sle_table.serial_and_batch_bundle == child.parent) & (sle_table.is_cancelled == 0)
)
)
timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & tie_condition
conditions = (
(child.item_code == self.sle.item_code)
@@ -957,9 +878,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
if timestamp_condition:
conditions &= timestamp_condition
if self.stock_closing_from_datetime:
conditions &= child.posting_datetime >= self.stock_closing_from_datetime
# MariaDB carries a row lock on the grouped query below; on postgres the caller
# (calculate_avg_rate) serializes via a txn-scoped advisory lock on (item, warehouse)
# instead of row-locking the whole history (FOR UPDATE is invalid with GROUP BY there).

View File

@@ -101,32 +101,6 @@ def validate_standard_cost_posting_date(sl_entries):
)
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
@@ -145,8 +119,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
for pair in sorted({(d.get("item_code"), d.get("warehouse")) for d in sl_entries}):
sle_processing_gate(*pair)
validate_stock_frozen_by_closing_entry(sl_entries)
cancelled = sl_entries[0].get("is_cancelled")
if cancelled:
validate_cancellation(sl_entries)