Compare commits

..

2 Commits

Author SHA1 Message Date
rohitwaghchaure
80de914b55 Merge pull request #56076 from frappe/mergify/bp/version-15/pr-56072
refactor: posting datetime for SABB (backport #49710 + #50248) (backport #56072)
2026-06-18 11:49:23 +05:30
rohitwaghchaure
1444837653 refactor: posting datetime for SABB (backport #49710 + #50248) (#56072)
* refactor: posting datetime for SABB (backport #49710 + #50248)

Backport of #49710 "refactor: posting datetime for SABB" to
version-15-hotfix, including the regression fix #50248
"fix: missed refactoring code".

Replaces Serial and Batch Bundle's `posting_date`/`posting_time` with a
single `posting_datetime` field and updates all readers/writers; adds a
patch to populate `posting_datetime` from the linked Stock Ledger Entry
and drop the now-unused single-column indexes.

Adapted to version-15-hotfix where the surrounding stock code had already
moved to `posting_datetime` (SLE) and diverged from develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: fix conflicts

* chore: fixed formatting

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 98caefea88)
2026-06-18 05:58:57 +00:00
18 changed files with 216 additions and 215 deletions

View File

@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate
class StockOverReturnError(frappe.ValidationError):
@@ -1180,8 +1180,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
"batches": data.get("batches"),
"serial_nos_valuation": data.get("serial_nos_valuation"),
"batches_valuation": data.get("batches_valuation"),
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
"voucher_type": parent_doc.doctype,
"voucher_no": parent_doc.name,
"voucher_detail_no": child_doc.name,

View File

@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, i
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
class SellingController(StockController):
@@ -1084,8 +1084,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
"voucher_type": parent.doctype,
"voucher_no": parent.name if parent.docstatus < 2 else None,
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
"posting_date": parent.posting_date,
"posting_time": parent.posting_time,
"posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time),
"qty": child.qty,
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
"company": parent.company,

View File

@@ -26,6 +26,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_type_of_transaction,
)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -282,8 +283,7 @@ class StockController(AccountsController):
):
bundle_details = {
"item_code": row.get("rm_item_code") or row.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,

View File

@@ -13,6 +13,7 @@ from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_auto_batch_nos,
get_available_serial_nos,
get_voucher_wise_serial_batch_from_bundle,
@@ -570,8 +571,7 @@ class SubcontractingController(StockController):
"qty": qty,
"serial_nos": serial_nos,
"batches": batches,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"voucher_type": "Subcontracting Receipt",
"do_not_submit": True,
"type_of_transaction": "Outward" if qty > 0 else "Inward",

View File

@@ -436,3 +436,4 @@ erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.clear_procedures_from_receivable_report
erpnext.patches.v16_0.migrate_address_contact_custom_fields
erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes

View File

@@ -0,0 +1,31 @@
import click
import frappe
def execute():
frappe.db.sql(
"""
UPDATE `tabSerial and Batch Bundle`
JOIN `tabStock Ledger Entry`
ON `tabSerial and Batch Bundle`.`name` = `tabStock Ledger Entry`.`serial_and_batch_bundle`
SET `tabSerial and Batch Bundle`.`posting_datetime` = `tabStock Ledger Entry`.`posting_datetime`
WHERE `tabStock Ledger Entry`.`is_cancelled` = 0
"""
)
drop_indexes()
def drop_indexes():
table = "tabSerial and Batch Bundle"
index_list = ["voucher_no_index", "item_code_index", "warehouse_index", "company_index"]
for index in index_list:
if not frappe.db.has_index(table, index):
continue
try:
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
click.echo(f"✓ dropped {index} index from {table}")
except Exception:
frappe.log_error("Failed to drop index")

View File

@@ -36,6 +36,11 @@ class DeprecatedSerialNoValuation:
# get rate from serial nos within same company
incoming_values = 0.0
posting_datetime = self.sle.posting_datetime
if not posting_datetime and self.sle.posting_date:
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
for serial_no in serial_nos:
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
if (
@@ -64,10 +69,7 @@ class DeprecatedSerialNoValuation:
& (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0)
& (table.is_cancelled == 0)
& (
table.posting_datetime
<= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
)
& (table.posting_datetime <= posting_datetime)
)
.orderby(table.posting_datetime, order=Order.desc)
.limit(1)
@@ -98,11 +100,8 @@ class DeprecatedBatchNoValuation:
sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = None
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if self.sle.posting_datetime:
posting_datetime = self.sle.posting_datetime
if not self.sle.creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
@@ -202,7 +201,11 @@ class DeprecatedBatchNoValuation:
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
posting_datetime = self.sle.posting_datetime
if not posting_datetime and self.sle.posting_date:
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if not self.sle.creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
@@ -266,7 +269,10 @@ class DeprecatedBatchNoValuation:
sle = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
posting_datetime = self.sle.posting_datetime
if not posting_datetime and self.sle.posting_date:
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if not self.sle.creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
@@ -308,19 +314,22 @@ class DeprecatedBatchNoValuation:
@deprecated
def set_balance_value_from_bundle(self) -> None:
from erpnext.stock.utils import get_combine_datetime
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
batch = frappe.qb.DocType("Batch")
timestamp_condition = CombineDatetime(bundle.posting_date, bundle.posting_time) < CombineDatetime(
self.sle.posting_date, self.sle.posting_time
)
posting_datetime = self.sle.posting_datetime
if not posting_datetime and self.sle.posting_date:
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
timestamp_condition = bundle.posting_datetime < posting_datetime
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(bundle.posting_date, bundle.posting_time)
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
) & (bundle.creation < self.sle.creation)
timestamp_condition |= (bundle.posting_datetime == posting_datetime) & (
bundle.creation < self.sle.creation
)
query = (
frappe.qb.from_(bundle)

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import datetime
from collections import OrderedDict, defaultdict
import frappe
@@ -10,7 +11,7 @@ from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form
from frappe.utils.data import add_days
from frappe.utils.data import DateTimeLikeObject, add_days
class UnableToSelectBatchError(frappe.ValidationError):
@@ -232,17 +233,18 @@ class Batch(Document):
@frappe.whitelist()
def get_batch_qty(
batch_no=None,
warehouse=None,
item_code=None,
creation=None,
posting_date=None,
posting_time=None,
ignore_voucher_nos=None,
for_stock_levels=False,
consider_negative_batches=False,
do_not_check_future_batches=False,
ignore_reserved_stock=False,
batch_no: str | None = None,
warehouse: str | None = None,
item_code: str | None = None,
creation: DateTimeLikeObject | None = None,
posting_datetime: DateTimeLikeObject | None = None,
posting_date: DateTimeLikeObject | None = None,
posting_time: datetime.timedelta | None = None,
ignore_voucher_nos: list | None = None,
for_stock_levels: bool = False,
consider_negative_batches: bool = False,
do_not_check_future_batches: bool = False,
ignore_reserved_stock: bool = False,
):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -255,6 +257,7 @@ def get_batch_qty(
:param for_stock_levels: True consider expired batches"""
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_auto_batch_nos,
)
@@ -264,8 +267,6 @@ def get_batch_qty(
"item_code": item_code,
"warehouse": warehouse,
"creation": creation,
"posting_date": posting_date,
"posting_time": posting_time,
"batch_no": batch_no,
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
"ignore_voucher_nos": ignore_voucher_nos,
@@ -276,6 +277,10 @@ def get_batch_qty(
}
)
kwargs["posting_datetime"] = posting_datetime
if not kwargs.get("posting_datetime") and posting_date:
kwargs["posting_datetime"] = combine_datetime(posting_date, posting_time)
batches = get_auto_batch_nos(kwargs)
if not (batch_no and warehouse):
@@ -357,6 +362,7 @@ def make_batch_bundle(
):
from frappe.utils import nowtime, today
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import combine_datetime
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
return (
@@ -364,8 +370,7 @@ def make_batch_bundle(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": today(),
"posting_time": nowtime(),
"posting_datetime": combine_datetime(today(), nowtime()),
"voucher_type": "Stock Entry",
"qty": qty,
"type_of_transaction": type_of_transaction,
@@ -476,9 +481,13 @@ def get_pos_reserved_batch_qty(filters):
def get_available_batches(kwargs):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_auto_batch_nos,
)
if kwargs.get("posting_date"):
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
batchwise_qty = OrderedDict()
batches = get_auto_batch_nos(kwargs)

View File

@@ -29,8 +29,7 @@
"voucher_no",
"voucher_detail_no",
"column_break_aouy",
"posting_date",
"posting_time",
"posting_datetime",
"returned_against",
"section_break_wzou",
"is_cancelled",
@@ -49,8 +48,7 @@
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"fetch_from": "item_code.item_group",
@@ -79,8 +77,7 @@
"in_standard_filter": 1,
"label": "Item Code",
"options": "Item",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
@@ -117,8 +114,7 @@
"in_standard_filter": 1,
"label": "Voucher No",
"no_copy": 1,
"options": "voucher_type",
"search_index": 1
"options": "voucher_type"
},
{
"default": "0",
@@ -188,8 +184,7 @@
"in_standard_filter": 1,
"label": "Warehouse",
"mandatory_depends_on": "eval:doc.type_of_transaction != \"Maintenance\"",
"options": "Warehouse",
"search_index": 1
"options": "Warehouse"
},
{
"fieldname": "type_of_transaction",
@@ -211,18 +206,6 @@
"fieldname": "section_break_wzou",
"fieldtype": "Section Break"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"no_copy": 1
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
"no_copy": 1
},
{
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
@@ -251,12 +234,17 @@
"label": "Naming Series",
"options": "\nSABB-.########",
"set_only_once": 1
},
{
"fieldname": "posting_datetime",
"fieldtype": "Datetime",
"label": "Posting Datetime"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-09-15 14:37:26.441742",
"modified": "2025-09-24 16:24:48.154853",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",

View File

@@ -74,8 +74,7 @@ class SerialandBatchBundle(Document):
item_group: DF.Link | None
item_name: DF.Data | None
naming_series: DF.Literal["", "SABB-.########"]
posting_date: DF.Date | None
posting_time: DF.Time | None
posting_datetime: DF.Datetime | None
returned_against: DF.Data | None
total_amount: DF.Float
total_qty: DF.Float
@@ -280,8 +279,7 @@ class SerialandBatchBundle(Document):
kwargs.update(
{
"voucher_no": self.voucher_no,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": self.posting_datetime,
}
)
@@ -332,8 +330,7 @@ class SerialandBatchBundle(Document):
kwargs = frappe._dict(
{
"item_code": self.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": self.posting_datetime,
"serial_nos": serial_nos,
"check_serial_nos": True,
}
@@ -669,8 +666,7 @@ class SerialandBatchBundle(Document):
def get_sle_for_outward_transaction(self):
sle = frappe._dict(
{
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": self.posting_datetime,
"item_code": self.item_code,
"warehouse": self.warehouse,
"serial_and_batch_bundle": self.name,
@@ -829,11 +825,10 @@ class SerialandBatchBundle(Document):
if not self.voucher_detail_no or self.voucher_detail_no != row.name:
values_to_set["voucher_detail_no"] = row.name
if parent.get("posting_date") and (not self.posting_date or self.posting_date != parent.posting_date):
values_to_set["posting_date"] = parent.posting_date or today()
if parent.get("posting_time") and (not self.posting_time or self.posting_time != parent.posting_time):
values_to_set["posting_time"] = parent.posting_time
if parent.get("posting_date") and parent.get("posting_time"):
posting_datetime = combine_datetime(parent.posting_date, parent.posting_time)
if not self.posting_datetime or self.posting_datetime != posting_datetime:
values_to_set["posting_datetime"] = posting_datetime
if row.get("doctype") == "Packed Item" and row.get("parent_detail_docname"):
values_to_set["voucher_detail_no"] = row.get("parent_detail_docname")
@@ -911,9 +906,7 @@ class SerialandBatchBundle(Document):
parent = frappe.qb.DocType("Serial and Batch Bundle")
child = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) > CombineDatetime(
self.posting_date, self.posting_time
)
timestamp_condition = parent.posting_datetime > self.posting_datetime
future_entries = (
frappe.qb.from_(parent)
@@ -2083,6 +2076,8 @@ def create_serial_batch_no_ledgers(
if parent_doc.get("doctype") == "Stock Entry":
warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time"))
doc = frappe.get_doc(
{
"doctype": "Serial and Batch Bundle",
@@ -2091,8 +2086,7 @@ def create_serial_batch_no_ledgers(
"warehouse": warehouse,
"is_rejected": child_row.is_rejected,
"type_of_transaction": type_of_transaction,
"posting_date": parent_doc.get("posting_date"),
"posting_time": parent_doc.get("posting_time"),
"posting_datetime": posting_datetime,
"company": parent_doc.get("company"),
}
)
@@ -2128,6 +2122,12 @@ def create_serial_batch_no_ledgers(
return doc
def combine_datetime(date, time=None):
from erpnext.stock.utils import get_combine_datetime
return get_combine_datetime(date, time)
def get_batch(item_code):
from erpnext.stock.doctype.batch.batch import make_batch
@@ -2180,8 +2180,8 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh
)
doc.voucher_detail_no = child_row.name
doc.posting_date = parent_doc.posting_date
doc.posting_time = parent_doc.posting_time
doc.posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time"))
doc.warehouse = warehouse or doc.warehouse
doc.set("entries", [])
@@ -2269,6 +2269,9 @@ def get_available_serial_nos(kwargs):
elif kwargs.based_on == "Expiry":
order_by = "amc_expiry_date"
if not kwargs.get("posting_datetime") and kwargs.get("posting_date"):
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
filters = {"item_code": kwargs.item_code}
# ignore_warehouse is used for backdated stock transactions
@@ -2285,10 +2288,8 @@ def get_available_serial_nos(kwargs):
if kwargs.get("ignore_serial_nos"):
ignore_serial_nos.extend(kwargs.get("ignore_serial_nos"))
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
ignore_serial_nos = list(set(ignore_serial_nos))
if kwargs.get("posting_datetime"):
time_based_serial_nos = get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos)
if not time_based_serial_nos:
@@ -2692,6 +2693,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
def get_auto_batch_nos(kwargs):
if not kwargs.get("posting_datetime") and kwargs.get("posting_date"):
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
available_batches = get_available_batches(kwargs)
qty = flt(kwargs.qty)
@@ -2721,7 +2725,7 @@ def get_auto_batch_nos(kwargs):
if kwargs.based_on == "Expiry":
available_batches = sorted(available_batches, key=lambda x: x.expiry_date or getdate("9999-12-31"))
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"):
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_datetime"):
filter_zero_near_batches(available_batches, kwargs)
if not kwargs.consider_negative_batches:
@@ -2737,8 +2741,7 @@ def get_auto_batch_nos(kwargs):
def filter_zero_near_batches(available_batches, kwargs):
kwargs.batch_no = [d.batch_no for d in available_batches]
del kwargs["posting_date"]
del kwargs["posting_time"]
del kwargs["posting_datetime"]
kwargs.do_not_check_future_batches = 1
available_batches_in_future = get_auto_batch_nos(kwargs)
@@ -2804,8 +2807,6 @@ def update_available_batches(available_batches, *reserved_batches) -> None:
def get_available_batches(kwargs):
from erpnext.stock.utils import get_combine_datetime
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
batch_table = frappe.qb.DocType("Batch")
@@ -2833,23 +2834,15 @@ def get_available_batches(kwargs):
if not kwargs.get("for_stock_levels"):
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
if kwargs.get("posting_datetime"):
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
if kwargs.get("creation"):
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
timestamp_condition |= (
stock_ledger_entry.posting_datetime
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
stock_ledger_entry.creation < kwargs.creation
)
query = query.where(timestamp_condition)
@@ -3035,15 +3028,14 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
serial_batch_table.incoming_rate,
bundle_table.voucher_detail_no,
bundle_table.voucher_no,
bundle_table.posting_date,
bundle_table.posting_time,
bundle_table.posting_datetime,
)
.where(
(bundle_table.docstatus == 1)
& (bundle_table.is_cancelled == 0)
& (bundle_table.type_of_transaction.isin(["Inward", "Outward"]))
)
.orderby(bundle_table.posting_date, bundle_table.posting_time)
.orderby(bundle_table.posting_datetime)
)
for key, val in kwargs.items():
@@ -3061,7 +3053,7 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
query = query.where(bundle_table[key].isin(val))
else:
query = query.where(bundle_table[key] == val)
elif key in ["posting_date", "posting_time"]:
elif key in ["posting_datetime"]:
query = query.where(bundle_table[key] >= val)
else:
if isinstance(val, list):
@@ -3073,8 +3065,6 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
def get_stock_ledgers_for_serial_nos(kwargs):
from erpnext.stock.utils import get_combine_datetime
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
query = (
@@ -3090,23 +3080,15 @@ def get_stock_ledgers_for_serial_nos(kwargs):
.orderby(stock_ledger_entry.creation)
)
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
if kwargs.get("posting_datetime"):
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
if kwargs.get("creation"):
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
timestamp_condition |= (
stock_ledger_entry.posting_datetime
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
stock_ledger_entry.creation < kwargs.creation
)
query = query.where(timestamp_condition)
@@ -3129,8 +3111,6 @@ def get_stock_ledgers_for_serial_nos(kwargs):
def get_stock_ledgers_batches(kwargs):
from erpnext.stock.utils import get_combine_datetime
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_table = frappe.qb.DocType("Batch")
@@ -3164,23 +3144,15 @@ def get_stock_ledgers_batches(kwargs):
if not kwargs.get("for_stock_levels"):
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
if kwargs.get("posting_datetime"):
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
if kwargs.get("creation"):
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
timestamp_condition |= (
stock_ledger_entry.posting_datetime
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
stock_ledger_entry.creation < kwargs.creation
)
query = query.where(timestamp_condition)
@@ -3268,3 +3240,7 @@ def get_stock_reco_details(voucher_detail_no):
],
as_dict=True,
)
def on_doctype_update():
frappe.db.add_index("Serial and Batch Bundle", ["item_code", "warehouse", "posting_datetime", "creation"])

View File

@@ -10,6 +10,7 @@ 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 (
add_serial_batch_ledgers,
combine_datetime,
make_batch_nos,
make_serial_nos,
)
@@ -1271,14 +1272,17 @@ def make_serial_batch_bundle(kwargs):
if kwargs.get("type_of_transaction"):
type_of_transaction = kwargs.get("type_of_transaction")
posting_datetime = None
if kwargs.get("posting_date"):
posting_datetime = combine_datetime(kwargs.posting_date, kwargs.posting_time or nowtime())
sb = SerialBatchCreation(
{
"item_code": kwargs.item_code,
"warehouse": kwargs.warehouse,
"voucher_type": kwargs.voucher_type,
"voucher_no": kwargs.voucher_no,
"posting_date": kwargs.posting_date,
"posting_time": kwargs.posting_time,
"posting_datetime": posting_datetime,
"qty": kwargs.qty,
"avg_rate": kwargs.rate,
"batches": kwargs.batches,

View File

@@ -52,7 +52,7 @@ from erpnext.stock.serial_batch_bundle import (
get_serial_or_batch_items,
)
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate
from erpnext.stock.utils import get_bin, get_incoming_rate
from erpnext.stock.utils import get_bin, get_combine_datetime, get_incoming_rate
class FinishedGoodError(frappe.ValidationError):
@@ -1527,8 +1527,7 @@ class StockEntry(StockController):
{
"item_code": row.item_code,
"warehouse": row.s_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": get_combine_datetime(self.posting_date, self.posting_time),
"voucher_type": self.doctype,
"voucher_detail_no": row.name,
"qty": row.transfer_qty * -1,
@@ -4058,8 +4057,7 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
"item_code": child.item_code,
"warehouse": child.warehouse,
"type_of_transaction": type_of_transaction,
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
}
)

View File

@@ -8,6 +8,7 @@ import frappe
from frappe.utils import cint, flt
import erpnext
from erpnext.stock.utils import get_combine_datetime
if TYPE_CHECKING:
from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
@@ -140,6 +141,10 @@ def make_stock_entry(**args):
elif args.batches:
batches = args.batches
posting_datetime = None
if args.posting_date and args.posting_time:
posting_datetime = get_combine_datetime(args.posting_date, args.posting_time)
bundle_id = (
SerialBatchCreation(
{
@@ -151,8 +156,7 @@ def make_stock_entry(**args):
"serial_nos": args.serial_no,
"type_of_transaction": "Outward" if args.source else "Inward",
"company": s.company,
"posting_date": s.posting_date,
"posting_time": s.posting_time,
"posting_datetime": posting_datetime,
"rate": args.rate or args.basic_rate,
"do_not_submit": True,
}

View File

@@ -13,6 +13,7 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_available_serial_nos,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -136,8 +137,7 @@ class StockReconciliation(StockController):
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
@@ -243,8 +243,7 @@ class StockReconciliation(StockController):
"doctype": "Serial and Batch Bundle",
"item_code": item.item_code,
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"voucher_type": self.doctype,
"type_of_transaction": "Outward",
}
@@ -262,8 +261,7 @@ class StockReconciliation(StockController):
{
"item_code": item.item_code,
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"ignore_warehouse": 1,
}
)

View File

@@ -19,7 +19,7 @@ def get_data(filters):
"Serial and Batch Bundle",
fields=[
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`posting_date`",
"`tabSerial and Batch Bundle`.`posting_datetime` as posting_date",
"`tabSerial and Batch Bundle`.`name`",
"`tabSerial and Batch Bundle`.`company`",
"`tabSerial and Batch Bundle`.`voucher_no`",
@@ -33,7 +33,7 @@ def get_data(filters):
"`tabSerial and Batch Entry`.`qty`",
],
filters=filter_conditions,
order_by="posting_date",
order_by="posting_datetime",
)
@@ -54,7 +54,7 @@ def get_filter_conditions(filters):
filter_conditions.append(
[
"Serial and Batch Bundle",
"posting_date",
"posting_datetime",
"between",
[filters.get("from_date"), filters.get("to_date")],
]

View File

@@ -5,7 +5,7 @@ from frappe import _, bold
from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today
from pypika import Order
from pypika.terms import ExistsCriterion
@@ -140,8 +140,7 @@ class SerialBatchBundle:
{
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": self.sle.posting_date,
"posting_time": self.sle.posting_time,
"posting_datetime": self.sle.posting_datetime,
"voucher_type": self.sle.voucher_type,
"voucher_no": self.sle.voucher_no,
"voucher_detail_no": self.sle.voucher_detail_no,
@@ -484,7 +483,7 @@ class SerialBatchBundle:
if status == "Delivered":
warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period")
if warranty_period:
warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period))
warranty_expiry_date = add_days(getdate(sle.posting_datetime), cint(warranty_period))
query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date)
query = query.set(sn_table.warranty_period, warranty_period)
else:
@@ -509,7 +508,7 @@ class SerialBatchBundle:
sle_doctype.voucher_no,
sle_doctype.is_cancelled,
sle_doctype.item_code,
sle_doctype.posting_date,
sle_doctype.posting_datetime,
sle_doctype.company,
)
.where(
@@ -663,7 +662,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
.on(bundle.name == bundle_child.parent)
.select(
bundle_child.serial_no,
Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"),
Max(bundle.posting_datetime).as_("max_posting_dt"),
)
.where(
(bundle.is_cancelled == 0)
@@ -681,13 +680,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
if self.sle.voucher_no:
latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no)
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
timestamp_condition = CombineDatetime(
bundle.posting_date, bundle.posting_time
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
if self.sle.posting_datetime:
timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime
latest_posting = latest_posting.where(timestamp_condition)
@@ -704,10 +698,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
.join(latest_posting)
.on(
(latest_posting.serial_no == bundle_child.serial_no)
& (
latest_posting.max_posting_dt
== CombineDatetime(bundle.posting_date, bundle.posting_time)
)
& (latest_posting.max_posting_dt == bundle.posting_datetime)
)
.select(
bundle_child.serial_no,
@@ -839,19 +830,13 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
child = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = ""
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
self.sle.posting_date, self.sle.posting_time
)
if self.sle.posting_datetime:
timestamp_condition = parent.posting_datetime < self.sle.posting_datetime
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(parent.posting_date, parent.posting_time)
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
) & (parent.creation < self.sle.creation)
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & (
parent.creation < self.sle.creation
)
query = (
frappe.qb.from_(parent)
@@ -1074,9 +1059,9 @@ class SerialBatchCreation:
self.__dict__.update(item_details)
def set_other_details(self):
if not self.get("posting_date"):
self.posting_date = today()
self.__dict__["posting_date"] = self.posting_date
if not self.get("posting_datetime"):
self.posting_datetime = now()
self.__dict__["posting_datetime"] = self.posting_datetime
if not self.get("actual_qty"):
qty = self.get("qty") or self.get("total_qty")
@@ -1101,8 +1086,7 @@ class SerialBatchCreation:
new_package.docstatus = 0
new_package.warehouse = self.warehouse
new_package.voucher_no = ""
new_package.posting_date = self.posting_date if hasattr(self, "posting_date") else today()
new_package.posting_time = self.posting_time if hasattr(self, "posting_time") else nowtime()
new_package.posting_datetime = self.posting_datetime if hasattr(self, "posting_datetime") else now()
new_package.type_of_transaction = self.type_of_transaction
new_package.returned_against = self.get("returned_against")
@@ -1242,9 +1226,8 @@ class SerialBatchCreation:
elif self.has_serial_no and not self.get("serial_nos"):
self.serial_nos = get_serial_nos_for_outward(kwargs)
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
if self.get("posting_date"):
kwargs["posting_date"] = self.get("posting_date")
kwargs["posting_time"] = self.get("posting_time")
if self.get("posting_datetime"):
kwargs["posting_datetime"] = self.get("posting_datetime")
self.batches = get_available_batches(kwargs)

View File

@@ -1975,9 +1975,6 @@ def get_valuation_rate(
# Get moving average rate of a specific batch number
if warehouse and serial_and_batch_bundle:
sabb = frappe.db.get_value(
"Serial and Batch Bundle", serial_and_batch_bundle, ["posting_date", "posting_time"], as_dict=True
)
batch_obj = BatchNoValuation(
sle=frappe._dict(
{
@@ -1985,8 +1982,9 @@ def get_valuation_rate(
"warehouse": warehouse,
"actual_qty": -1,
"serial_and_batch_bundle": serial_and_batch_bundle,
"posting_date": sabb.posting_date,
"posting_time": sabb.posting_time,
"posting_datetime": frappe.get_value(
"Serial and Batch Bundle", serial_and_batch_bundle, "posting_datetime"
),
}
)
)

View File

@@ -2,12 +2,14 @@
# License: GNU General Public License v3. See license.txt
import datetime
import json
import frappe
from frappe import _
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
from frappe.utils.data import DateTimeLikeObject
import erpnext
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
@@ -94,13 +96,13 @@ def get_stock_value_on(
@frappe.whitelist()
def get_stock_balance(
item_code,
warehouse,
posting_date=None,
posting_time=None,
with_valuation_rate=False,
with_serial_no=False,
inventory_dimensions_dict=None,
item_code: str,
warehouse: str | None,
posting_date: DateTimeLikeObject | None = None,
posting_time: DateTimeLikeObject | datetime.timedelta | None = None,
with_valuation_rate: bool = False,
with_serial_no: bool = False,
inventory_dimensions_dict: dict | None = None,
):
"""Returns stock balance quantity at given warehouse on given posting date or current date.
@@ -146,8 +148,7 @@ def get_stock_balance(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": posting_date,
"posting_time": posting_time,
"posting_datetime": get_combine_datetime(posting_date, posting_time),
"ignore_warehouse": 1,
}
)
@@ -247,13 +248,16 @@ def _create_bin(item_code, warehouse):
@frappe.whitelist()
def get_incoming_rate(args, raise_error_if_no_rate=True, fallbacks: bool = True):
def get_incoming_rate(args: dict | str, raise_error_if_no_rate: bool = True, fallbacks: bool = True):
"""Get Incoming Rate based on valuation method"""
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
if isinstance(args, str):
args = json.loads(args)
if not args.get("posting_datetime") and args.get("posting_date"):
args["posting_datetime"] = get_combine_datetime(args.get("posting_date"), args.get("posting_time"))
in_rate = None
item_details = frappe.get_cached_value(