mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-18 00:55:02 +00:00
perf: new column posting datetime in SLE to optimize stock ledger related queries
(cherry picked from commit d80ca523a4)
This commit is contained in:
@@ -965,7 +965,7 @@ class GrossProfitGenerator(object):
|
|||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
)
|
)
|
||||||
.orderby(sle.item_code)
|
.orderby(sle.item_code)
|
||||||
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
|
.orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc)
|
||||||
.run(as_dict=True)
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -931,46 +931,6 @@ def get_currency_precision():
|
|||||||
return precision
|
return precision
|
||||||
|
|
||||||
|
|
||||||
def get_stock_rbnb_difference(posting_date, company):
|
|
||||||
stock_items = frappe.db.sql_list(
|
|
||||||
"""select distinct item_code
|
|
||||||
from `tabStock Ledger Entry` where company=%s""",
|
|
||||||
company,
|
|
||||||
)
|
|
||||||
|
|
||||||
pr_valuation_amount = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor)
|
|
||||||
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
|
|
||||||
where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s
|
|
||||||
and pr.posting_date <= %s and pr_item.item_code in (%s)"""
|
|
||||||
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
|
|
||||||
tuple([company, posting_date] + stock_items),
|
|
||||||
)[0][0]
|
|
||||||
|
|
||||||
pi_valuation_amount = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor)
|
|
||||||
from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi
|
|
||||||
where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s
|
|
||||||
and pi.posting_date <= %s and pi_item.item_code in (%s)"""
|
|
||||||
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
|
|
||||||
tuple([company, posting_date] + stock_items),
|
|
||||||
)[0][0]
|
|
||||||
|
|
||||||
# Balance should be
|
|
||||||
stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2)
|
|
||||||
|
|
||||||
# Balance as per system
|
|
||||||
stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value(
|
|
||||||
"Company", company, "abbr"
|
|
||||||
)
|
|
||||||
sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False)
|
|
||||||
|
|
||||||
# Amount should be credited
|
|
||||||
return flt(stock_rbnb) + flt(sys_bal)
|
|
||||||
|
|
||||||
|
|
||||||
def get_held_invoices(party_type, party):
|
def get_held_invoices(party_type, party):
|
||||||
"""
|
"""
|
||||||
Returns a list of names Purchase Invoices for the given party that are on hold
|
Returns a list of names Purchase Invoices for the given party that are on hold
|
||||||
@@ -1372,8 +1332,7 @@ def sort_stock_vouchers_by_posting_date(
|
|||||||
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||||
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||||
.groupby(sle.voucher_type, sle.voucher_no)
|
.groupby(sle.voucher_type, sle.voucher_no)
|
||||||
.orderby(sle.posting_date)
|
.orderby(sle.posting_datetime)
|
||||||
.orderby(sle.posting_time)
|
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||||
|
|||||||
@@ -978,8 +978,7 @@ def get_valuation_rate(data):
|
|||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select(sle.valuation_rate)
|
.select(sle.valuation_rate)
|
||||||
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
||||||
.orderby(sle.posting_date, order=frappe.qb.desc)
|
.orderby(sle.posting_datetime, order=frappe.qb.desc)
|
||||||
.orderby(sle.posting_time, order=frappe.qb.desc)
|
|
||||||
.orderby(sle.creation, order=frappe.qb.desc)
|
.orderby(sle.creation, order=frappe.qb.desc)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def get_data(filters):
|
|||||||
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
|
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
|
||||||
|
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
|
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
||||||
)
|
)
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
|||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||||
|
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes
|
||||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||||
@@ -361,4 +362,4 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2
|
|||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
|
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
|
||||||
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
||||||
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.sql(
|
||||||
|
"""
|
||||||
|
UPDATE `tabStock Ledger Entry`
|
||||||
|
SET posting_datetime = timestamp(posting_date, posting_time)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
drop_indexes()
|
||||||
|
|
||||||
|
|
||||||
|
def drop_indexes():
|
||||||
|
if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`")
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, cint, cstr, flt, today
|
from frappe.utils import add_days, cint, cstr, flt, today, nowtime
|
||||||
from pypika import functions as fn
|
from pypika import functions as fn
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -2224,6 +2224,95 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr.reload()
|
pr.reload()
|
||||||
self.assertEqual(pr.per_billed, 100)
|
self.assertEqual(pr.per_billed, 100)
|
||||||
|
|
||||||
|
def test_sle_qty_after_transaction(self):
|
||||||
|
item = make_item(
|
||||||
|
"_Test Item Qty After Transaction",
|
||||||
|
properties={"is_stock_item": 1, "valuation_method": "FIFO"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
posting_date = today()
|
||||||
|
posting_time = nowtime()
|
||||||
|
|
||||||
|
# Step 1: Create Purchase Receipt
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(9):
|
||||||
|
pr.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 100,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"cost_center": pr.items[0].cost_center,
|
||||||
|
"expense_account": pr.items[0].expense_account,
|
||||||
|
"uom": pr.items[0].uom,
|
||||||
|
"stock_uom": pr.items[0].stock_uom,
|
||||||
|
"conversion_factor": pr.items[0].conversion_factor,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.items), 10)
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["qty_after_transaction", "creation", "posting_datetime"],
|
||||||
|
filters={"voucher_no": pr.name, "is_cancelled": 0},
|
||||||
|
order_by="creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, d in enumerate(data):
|
||||||
|
self.assertEqual(d.qty_after_transaction, 1 + index)
|
||||||
|
|
||||||
|
# Step 2: Create Purchase Receipt
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(9):
|
||||||
|
pr.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 100,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"cost_center": pr.items[0].cost_center,
|
||||||
|
"expense_account": pr.items[0].expense_account,
|
||||||
|
"uom": pr.items[0].uom,
|
||||||
|
"stock_uom": pr.items[0].stock_uom,
|
||||||
|
"conversion_factor": pr.items[0].conversion_factor,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.items), 10)
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["qty_after_transaction", "creation", "posting_datetime"],
|
||||||
|
filters={"voucher_no": pr.name, "is_cancelled": 0},
|
||||||
|
order_by="creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, d in enumerate(data):
|
||||||
|
self.assertEqual(d.qty_after_transaction, 11 + index)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -0,0 +1,590 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
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,
|
||||||
|
make_batch_nos,
|
||||||
|
make_serial_nos,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerialandBatchBundle(FrappeTestCase):
|
||||||
|
def test_inward_outward_serial_valuation(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
serial_item_code = "New Serial No Valuation 1"
|
||||||
|
make_item(
|
||||||
|
serial_item_code,
|
||||||
|
{
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TEST-SER-VAL-.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_no1 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_no2 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=serial_item_code,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=1,
|
||||||
|
rate=1500,
|
||||||
|
serial_no=[serial_no2],
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_value_difference, 2), -300)
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=serial_item_code,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=1,
|
||||||
|
rate=1500,
|
||||||
|
serial_no=[serial_no1],
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_value_difference, 2), -500)
|
||||||
|
|
||||||
|
def test_inward_outward_batch_valuation(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
batch_item_code = "New Batch No Valuation 1"
|
||||||
|
make_item(
|
||||||
|
batch_item_code,
|
||||||
|
{
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TEST-BATTCCH-VAL-.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=500
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no1 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=300
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no2 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=batch_item_code,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=10,
|
||||||
|
rate=1500,
|
||||||
|
batch_no=batch_no2,
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_value_difference, 2), -3000)
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=batch_item_code,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=10,
|
||||||
|
rate=1500,
|
||||||
|
batch_no=batch_no1,
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_value_difference, 2), -5000)
|
||||||
|
|
||||||
|
def test_old_batch_valuation(self):
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
|
batch_item_code = "Old Batch Item Valuation 1"
|
||||||
|
make_item(
|
||||||
|
batch_item_code,
|
||||||
|
{
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_id = "Old Batch 1"
|
||||||
|
if not frappe.db.exists("Batch", batch_id):
|
||||||
|
batch_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Batch",
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"item": batch_item_code,
|
||||||
|
"use_batchwise_valuation": 0,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
self.assertTrue(batch_doc.use_batchwise_valuation)
|
||||||
|
batch_doc.db_set("use_batchwise_valuation", 0)
|
||||||
|
|
||||||
|
stock_queue = []
|
||||||
|
qty_after_transaction = 0
|
||||||
|
balance_value = 0
|
||||||
|
for qty, valuation in {10: 100, 20: 200}.items():
|
||||||
|
stock_queue.append([qty, valuation])
|
||||||
|
qty_after_transaction += qty
|
||||||
|
balance_value += qty_after_transaction * valuation
|
||||||
|
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Stock Ledger Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"batch_no": batch_id,
|
||||||
|
"incoming_rate": valuation,
|
||||||
|
"qty_after_transaction": qty_after_transaction,
|
||||||
|
"stock_value_difference": valuation * qty,
|
||||||
|
"balance_value": balance_value,
|
||||||
|
"valuation_rate": balance_value / qty_after_transaction,
|
||||||
|
"actual_qty": qty,
|
||||||
|
"item_code": batch_item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"stock_queue": json.dumps(stock_queue),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.flags.ignore_mandatory = True
|
||||||
|
doc.flags.ignore_links = True
|
||||||
|
doc.flags.ignore_validate = True
|
||||||
|
doc.submit()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
bundle_doc = make_serial_batch_bundle(
|
||||||
|
{
|
||||||
|
"item_code": batch_item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"qty": -10,
|
||||||
|
"batches": frappe._dict({batch_id: 10}),
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_doc.reload()
|
||||||
|
for row in bundle_doc.entries:
|
||||||
|
self.assertEqual(flt(row.stock_value_difference, 2), -1666.67)
|
||||||
|
|
||||||
|
bundle_doc.flags.ignore_permissions = True
|
||||||
|
bundle_doc.flags.ignore_mandatory = True
|
||||||
|
bundle_doc.flags.ignore_links = True
|
||||||
|
bundle_doc.flags.ignore_validate = True
|
||||||
|
bundle_doc.submit()
|
||||||
|
|
||||||
|
bundle_doc = make_serial_batch_bundle(
|
||||||
|
{
|
||||||
|
"item_code": batch_item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"qty": -20,
|
||||||
|
"batches": frappe._dict({batch_id: 20}),
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_doc.reload()
|
||||||
|
for row in bundle_doc.entries:
|
||||||
|
self.assertEqual(flt(row.stock_value_difference, 2), -3333.33)
|
||||||
|
|
||||||
|
bundle_doc.flags.ignore_permissions = True
|
||||||
|
bundle_doc.flags.ignore_mandatory = True
|
||||||
|
bundle_doc.flags.ignore_links = True
|
||||||
|
bundle_doc.flags.ignore_validate = True
|
||||||
|
bundle_doc.submit()
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
|
|
||||||
|
def test_old_serial_no_valuation(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
serial_no_item_code = "Old Serial No Item Valuation 1"
|
||||||
|
make_item(
|
||||||
|
serial_no_item_code,
|
||||||
|
{
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TEST-SER-VALL-.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=serial_no_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
|
|
||||||
|
serial_no_id = "Old Serial No 1"
|
||||||
|
if not frappe.db.exists("Serial No", serial_no_id):
|
||||||
|
sn_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial No",
|
||||||
|
"serial_no": serial_no_id,
|
||||||
|
"item_code": serial_no_item_code,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
sn_doc.db_set(
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"purchase_rate": 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Stock Ledger Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"serial_no": serial_no_id,
|
||||||
|
"incoming_rate": 100,
|
||||||
|
"qty_after_transaction": 1,
|
||||||
|
"stock_value_difference": 100,
|
||||||
|
"balance_value": 100,
|
||||||
|
"valuation_rate": 100,
|
||||||
|
"actual_qty": 1,
|
||||||
|
"item_code": serial_no_item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.flags.ignore_mandatory = True
|
||||||
|
doc.flags.ignore_links = True
|
||||||
|
doc.flags.ignore_validate = True
|
||||||
|
doc.submit()
|
||||||
|
|
||||||
|
bundle_doc = make_serial_batch_bundle(
|
||||||
|
{
|
||||||
|
"item_code": serial_no_item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"qty": -1,
|
||||||
|
"serial_nos": [serial_no_id],
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_doc.reload()
|
||||||
|
for row in bundle_doc.entries:
|
||||||
|
self.assertEqual(flt(row.stock_value_difference, 2), -100.00)
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
|
|
||||||
|
def test_batch_not_belong_to_serial_no(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
serial_and_batch_code = "New Serial No Valuation 1"
|
||||||
|
make_item(
|
||||||
|
serial_and_batch_code,
|
||||||
|
{
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TEST-SER-VALL-.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TEST-SNBAT-VAL-.#####",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial and Batch Bundle",
|
||||||
|
"item_code": serial_and_batch_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"qty": -1,
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.append(
|
||||||
|
"entries",
|
||||||
|
{
|
||||||
|
"batch_no": batch_no,
|
||||||
|
"serial_no": serial_no,
|
||||||
|
"qty": -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Batch does not belong to serial no
|
||||||
|
self.assertRaises(frappe.exceptions.ValidationError, doc.save)
|
||||||
|
|
||||||
|
def test_auto_delete_draft_serial_and_batch_bundle(self):
|
||||||
|
serial_and_batch_code = "New Serial No Auto Delete 1"
|
||||||
|
make_item(
|
||||||
|
serial_and_batch_code,
|
||||||
|
{
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TEST-SER-VALL-.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ste = make_stock_entry(
|
||||||
|
item_code=serial_and_batch_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=1,
|
||||||
|
rate=500,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_no = "SN-TEST-AUTO-DEL"
|
||||||
|
if not frappe.db.exists("Serial No", serial_no):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial No",
|
||||||
|
"serial_no": serial_no,
|
||||||
|
"item_code": serial_and_batch_code,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
bundle_doc = make_serial_batch_bundle(
|
||||||
|
{
|
||||||
|
"item_code": serial_and_batch_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": ste.posting_date,
|
||||||
|
"posting_time": ste.posting_time,
|
||||||
|
"qty": 1,
|
||||||
|
"serial_nos": [serial_no],
|
||||||
|
"type_of_transaction": "Inward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_doc.reload()
|
||||||
|
ste.items[0].serial_and_batch_bundle = bundle_doc.name
|
||||||
|
ste.save()
|
||||||
|
ste.reload()
|
||||||
|
|
||||||
|
ste.delete()
|
||||||
|
self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name))
|
||||||
|
|
||||||
|
def test_serial_and_batch_bundle_company(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
item = make_item(
|
||||||
|
"Test Serial and Batch Bundle Company Item",
|
||||||
|
properties={
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TT-SER-VAL-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=3,
|
||||||
|
rate=500,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for serial_no in ["TT-SER-VAL-00001", "TT-SER-VAL-00002", "TT-SER-VAL-00003"]:
|
||||||
|
entries.append(frappe._dict({"serial_no": serial_no, "qty": 1}))
|
||||||
|
|
||||||
|
if not frappe.db.exists("Serial No", serial_no):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial No",
|
||||||
|
"serial_no": serial_no,
|
||||||
|
"item_code": item,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
item_row = pr.items[0]
|
||||||
|
item_row.type_of_transaction = "Inward"
|
||||||
|
item_row.is_rejected = 0
|
||||||
|
sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC")
|
||||||
|
self.assertEqual(sn_doc.company, "_Test Company")
|
||||||
|
|
||||||
|
def test_auto_cancel_serial_and_batch(self):
|
||||||
|
item_code = make_item(
|
||||||
|
properties={"has_serial_no": 1, "serial_no_series": "ATC-TT-SER-VAL-.#####"}
|
||||||
|
).name
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=5,
|
||||||
|
rate=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = se.items[0].serial_and_batch_bundle
|
||||||
|
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
|
||||||
|
self.assertEqual(docstatus, 1)
|
||||||
|
|
||||||
|
se.cancel()
|
||||||
|
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
|
||||||
|
self.assertEqual(docstatus, 2)
|
||||||
|
|
||||||
|
def test_batch_duplicate_entry(self):
|
||||||
|
item_code = make_item(properties={"has_batch_no": 1}).name
|
||||||
|
|
||||||
|
batch_id = "TEST-BATTCCH-VAL-00001"
|
||||||
|
batch_nos = [{"batch_no": batch_id, "qty": 1}]
|
||||||
|
|
||||||
|
make_batch_nos(item_code, batch_nos)
|
||||||
|
self.assertTrue(frappe.db.exists("Batch", batch_id))
|
||||||
|
|
||||||
|
batch_id = "TEST-BATTCCH-VAL-00001"
|
||||||
|
batch_nos = [{"batch_no": batch_id, "qty": 1}]
|
||||||
|
|
||||||
|
# Shouldn't throw duplicate entry error
|
||||||
|
make_batch_nos(item_code, batch_nos)
|
||||||
|
self.assertTrue(frappe.db.exists("Batch", batch_id))
|
||||||
|
|
||||||
|
def test_serial_no_duplicate_entry(self):
|
||||||
|
item_code = make_item(properties={"has_serial_no": 1}).name
|
||||||
|
|
||||||
|
serial_no_id = "TEST-SNID-VAL-00001"
|
||||||
|
serial_nos = [{"serial_no": serial_no_id, "qty": 1}]
|
||||||
|
|
||||||
|
make_serial_nos(item_code, serial_nos)
|
||||||
|
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
|
||||||
|
|
||||||
|
serial_no_id = "TEST-SNID-VAL-00001"
|
||||||
|
serial_nos = [{"batch_no": serial_no_id, "qty": 1}]
|
||||||
|
|
||||||
|
# Shouldn't throw duplicate entry error
|
||||||
|
make_serial_nos(item_code, serial_nos)
|
||||||
|
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||||
|
def test_duplicate_serial_and_batch_bundle(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name
|
||||||
|
|
||||||
|
serial_no = f"{item_code}-001"
|
||||||
|
serial_nos = [{"serial_no": serial_no, "qty": 1}]
|
||||||
|
make_serial_nos(item_code, serial_nos)
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no])
|
||||||
|
pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True)
|
||||||
|
|
||||||
|
pr1.reload()
|
||||||
|
pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle
|
||||||
|
|
||||||
|
self.assertRaises(frappe.exceptions.ValidationError, pr2.save)
|
||||||
|
|
||||||
|
|
||||||
|
def get_batch_from_bundle(bundle):
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||||
|
|
||||||
|
batches = get_batch_nos(bundle)
|
||||||
|
|
||||||
|
return list(batches.keys())[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_serial_nos_from_bundle(bundle):
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_serial_nos
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos(bundle)
|
||||||
|
return sorted(serial_nos) if serial_nos else []
|
||||||
|
|
||||||
|
|
||||||
|
def make_serial_batch_bundle(kwargs):
|
||||||
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
type_of_transaction = "Inward" if kwargs.qty > 0 else "Outward"
|
||||||
|
if kwargs.get("type_of_transaction"):
|
||||||
|
type_of_transaction = kwargs.get("type_of_transaction")
|
||||||
|
|
||||||
|
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,
|
||||||
|
"qty": kwargs.qty,
|
||||||
|
"avg_rate": kwargs.rate,
|
||||||
|
"batches": kwargs.batches,
|
||||||
|
"serial_nos": kwargs.serial_nos,
|
||||||
|
"type_of_transaction": type_of_transaction,
|
||||||
|
"company": kwargs.company or "_Test Company",
|
||||||
|
"do_not_submit": kwargs.do_not_submit,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not kwargs.get("do_not_save"):
|
||||||
|
return sb.make_serial_and_batch_bundle()
|
||||||
|
|
||||||
|
return sb
|
||||||
@@ -1721,9 +1721,22 @@ class StockEntry(StockController):
|
|||||||
if qty <= 0:
|
if qty <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
fg_qty = batch_qty
|
fg_qty = batch_qty
|
||||||
if batch_qty >= qty:
|
if batch_qty >= qty:
|
||||||
fg_qty = qty
|
fg_qty = qty
|
||||||
|
=======
|
||||||
|
id = create_serial_and_batch_bundle(
|
||||||
|
self,
|
||||||
|
row,
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": self.pro_doc.production_item,
|
||||||
|
"warehouse": args.get("to_warehouse"),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
|
|
||||||
qty -= batch_qty
|
qty -= batch_qty
|
||||||
args["qty"] = fg_qty
|
args["qty"] = fg_qty
|
||||||
@@ -1969,7 +1982,11 @@ class StockEntry(StockController):
|
|||||||
"to_warehouse": "",
|
"to_warehouse": "",
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
"item_name": item.item_name,
|
"item_name": item.item_name,
|
||||||
|
<<<<<<< HEAD
|
||||||
"batch_no": item.batch_no,
|
"batch_no": item.batch_no,
|
||||||
|
=======
|
||||||
|
"serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"),
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
"description": item.description,
|
"description": item.description,
|
||||||
"stock_uom": item.stock_uom,
|
"stock_uom": item.stock_uom,
|
||||||
"expense_account": item.expense_account,
|
"expense_account": item.expense_account,
|
||||||
@@ -2377,6 +2394,39 @@ class StockEntry(StockController):
|
|||||||
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
|
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
|
||||||
|
|
||||||
def set_serial_no_batch_for_finished_good(self):
|
def set_serial_no_batch_for_finished_good(self):
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
if not (
|
||||||
|
(self.pro_doc.has_serial_no or self.pro_doc.has_batch_no)
|
||||||
|
and frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
for d in self.items:
|
||||||
|
if (
|
||||||
|
d.is_finished_item
|
||||||
|
and d.item_code == self.pro_doc.production_item
|
||||||
|
and not d.serial_and_batch_bundle
|
||||||
|
):
|
||||||
|
serial_nos = self.get_available_serial_nos()
|
||||||
|
if serial_nos:
|
||||||
|
row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]})
|
||||||
|
|
||||||
|
id = create_serial_and_batch_bundle(
|
||||||
|
self,
|
||||||
|
row,
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"warehouse": d.t_warehouse,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
d.serial_and_batch_bundle = id
|
||||||
|
|
||||||
|
def get_available_serial_nos(self) -> List[str]:
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
serial_nos = []
|
serial_nos = []
|
||||||
if self.pro_doc.serial_no:
|
if self.pro_doc.serial_no:
|
||||||
serial_nos = self.get_serial_nos_for_fg() or []
|
serial_nos = self.get_serial_nos_for_fg() or []
|
||||||
@@ -2855,3 +2905,91 @@ def get_stock_entry_data(work_order):
|
|||||||
)
|
)
|
||||||
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
|
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
|
||||||
).run(as_dict=1)
|
).run(as_dict=1)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
voucher_nos = [row.get("name") for row in data if row.get("name")]
|
||||||
|
if voucher_nos:
|
||||||
|
bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=voucher_nos)
|
||||||
|
for row in data:
|
||||||
|
key = (row.item_code, row.warehouse, row.name)
|
||||||
|
if row.purpose != "Material Transfer for Manufacture":
|
||||||
|
key = (row.item_code, row.s_warehouse, row.name)
|
||||||
|
|
||||||
|
if bundle_data.get(key):
|
||||||
|
row.update(bundle_data.get(key))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=None):
|
||||||
|
item_details = frappe.get_cached_value(
|
||||||
|
"Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (item_details.has_serial_no or item_details.has_batch_no):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not type_of_transaction:
|
||||||
|
type_of_transaction = "Inward"
|
||||||
|
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial and Batch Bundle",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if row.serial_nos and row.batches_to_be_consume:
|
||||||
|
doc.has_serial_no = 1
|
||||||
|
doc.has_batch_no = 1
|
||||||
|
batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row)
|
||||||
|
for batch_no, qty in row.batches_to_be_consume.items():
|
||||||
|
|
||||||
|
while qty > 0:
|
||||||
|
qty -= 1
|
||||||
|
doc.append(
|
||||||
|
"entries",
|
||||||
|
{
|
||||||
|
"batch_no": batch_no,
|
||||||
|
"serial_no": batchwise_serial_nos.get(batch_no).pop(0),
|
||||||
|
"warehouse": row.warehouse,
|
||||||
|
"qty": -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif row.serial_nos:
|
||||||
|
doc.has_serial_no = 1
|
||||||
|
for serial_no in row.serial_nos:
|
||||||
|
doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
|
||||||
|
|
||||||
|
elif row.batches_to_be_consume:
|
||||||
|
doc.has_batch_no = 1
|
||||||
|
for batch_no, qty in row.batches_to_be_consume.items():
|
||||||
|
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
|
||||||
|
|
||||||
|
return doc.insert(ignore_permissions=True).name
|
||||||
|
|
||||||
|
|
||||||
|
def get_batchwise_serial_nos(item_code, row):
|
||||||
|
batchwise_serial_nos = {}
|
||||||
|
|
||||||
|
for batch_no in row.batches_to_be_consume:
|
||||||
|
serial_nos = frappe.get_all(
|
||||||
|
"Serial No",
|
||||||
|
filters={"item_code": item_code, "batch_no": batch_no, "name": ("in", row.serial_nos)},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serial_nos:
|
||||||
|
batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos])
|
||||||
|
|
||||||
|
return batchwise_serial_nos
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"warehouse",
|
"warehouse",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
|
"posting_datetime",
|
||||||
"is_adjustment_entry",
|
"is_adjustment_entry",
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
"voucher_type",
|
"voucher_type",
|
||||||
@@ -96,7 +97,6 @@
|
|||||||
"oldfieldtype": "Date",
|
"oldfieldtype": "Date",
|
||||||
"print_width": "100px",
|
"print_width": "100px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"search_index": 1,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -249,7 +249,6 @@
|
|||||||
"options": "Company",
|
"options": "Company",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"search_index": 1,
|
|
||||||
"width": "150px"
|
"width": "150px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -316,6 +315,11 @@
|
|||||||
"fieldname": "is_adjustment_entry",
|
"fieldname": "is_adjustment_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Adjustment Entry"
|
"label": "Is Adjustment Entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_datetime",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Posting Datetime"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
@@ -324,7 +328,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-13 09:56:13.021696",
|
"modified": "2024-02-07 09:18:13.999231",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Ledger Entry",
|
"name": "Stock Ledger Entry",
|
||||||
|
|||||||
@@ -28,6 +28,50 @@ exclude_from_linked_with = True
|
|||||||
|
|
||||||
|
|
||||||
class StockLedgerEntry(Document):
|
class StockLedgerEntry(Document):
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
actual_qty: DF.Float
|
||||||
|
auto_created_serial_and_batch_bundle: DF.Check
|
||||||
|
batch_no: DF.Data | None
|
||||||
|
company: DF.Link | None
|
||||||
|
dependant_sle_voucher_detail_no: DF.Data | None
|
||||||
|
fiscal_year: DF.Data | None
|
||||||
|
has_batch_no: DF.Check
|
||||||
|
has_serial_no: DF.Check
|
||||||
|
incoming_rate: DF.Currency
|
||||||
|
is_adjustment_entry: DF.Check
|
||||||
|
is_cancelled: DF.Check
|
||||||
|
item_code: DF.Link | None
|
||||||
|
outgoing_rate: DF.Currency
|
||||||
|
posting_date: DF.Date | None
|
||||||
|
posting_datetime: DF.Datetime | None
|
||||||
|
posting_time: DF.Time | None
|
||||||
|
project: DF.Link | None
|
||||||
|
qty_after_transaction: DF.Float
|
||||||
|
recalculate_rate: DF.Check
|
||||||
|
serial_and_batch_bundle: DF.Link | None
|
||||||
|
serial_no: DF.LongText | None
|
||||||
|
stock_queue: DF.Text | None
|
||||||
|
stock_uom: DF.Link | None
|
||||||
|
stock_value: DF.Currency
|
||||||
|
stock_value_difference: DF.Currency
|
||||||
|
to_rename: DF.Check
|
||||||
|
valuation_rate: DF.Currency
|
||||||
|
voucher_detail_no: DF.Data | None
|
||||||
|
voucher_no: DF.DynamicLink | None
|
||||||
|
voucher_type: DF.Link | None
|
||||||
|
warehouse: DF.Link | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
"""
|
"""
|
||||||
Temporarily name doc for fast insertion
|
Temporarily name doc for fast insertion
|
||||||
@@ -52,6 +96,12 @@ class StockLedgerEntry(Document):
|
|||||||
self.validate_with_last_transaction_posting_time()
|
self.validate_with_last_transaction_posting_time()
|
||||||
self.validate_inventory_dimension_negative_stock()
|
self.validate_inventory_dimension_negative_stock()
|
||||||
|
|
||||||
|
def set_posting_datetime(self):
|
||||||
|
from erpnext.stock.utils import get_combine_datetime
|
||||||
|
|
||||||
|
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
||||||
|
self.db_set("posting_datetime", self.posting_datetime)
|
||||||
|
|
||||||
def validate_inventory_dimension_negative_stock(self):
|
def validate_inventory_dimension_negative_stock(self):
|
||||||
if self.is_cancelled:
|
if self.is_cancelled:
|
||||||
return
|
return
|
||||||
@@ -122,6 +172,7 @@ class StockLedgerEntry(Document):
|
|||||||
return inv_dimension_dict
|
return inv_dimension_dict
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.set_posting_datetime()
|
||||||
self.check_stock_frozen_date()
|
self.check_stock_frozen_date()
|
||||||
self.calculate_batch_qty()
|
self.calculate_batch_qty()
|
||||||
|
|
||||||
@@ -293,9 +344,7 @@ class StockLedgerEntry(Document):
|
|||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
def on_doctype_update():
|
||||||
frappe.db.add_index(
|
|
||||||
"Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index"
|
|
||||||
)
|
|
||||||
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
|
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
|
||||||
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
|
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
|
||||||
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
|
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
|
||||||
|
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])
|
||||||
|
|||||||
@@ -1066,7 +1066,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
|||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select("qty_after_transaction")
|
.select("qty_after_transaction")
|
||||||
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
|
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
.orderby(sle.posting_datetime)
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
).run(pluck=True)
|
).run(pluck=True)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import Field
|
from frappe.query_builder import Field
|
||||||
from frappe.query_builder.functions import CombineDatetime, Min
|
from frappe.query_builder.functions import Min
|
||||||
from frappe.utils import add_days, getdate, today
|
from frappe.utils import add_days, getdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -75,7 +75,7 @@ def get_data(report_filters):
|
|||||||
& (sle.company == report_filters.company)
|
& (sle.company == report_filters.company)
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
)
|
)
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation)
|
.orderby(sle.posting_datetime, sle.creation)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
|
|||||||
@@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.force_index("posting_sort_index")
|
|
||||||
.left_join(sle2)
|
.left_join(sle2)
|
||||||
.on(
|
.on(
|
||||||
(sle.item_code == sle2.item_code)
|
(sle.item_code == sle2.item_code)
|
||||||
& (sle.warehouse == sle2.warehouse)
|
& (sle.warehouse == sle2.warehouse)
|
||||||
& (sle.posting_date < sle2.posting_date)
|
& (sle.posting_datetime < sle2.posting_datetime)
|
||||||
& (sle.posting_time < sle2.posting_time)
|
|
||||||
& (sle.name < sle2.name)
|
& (sle.name < sle2.name)
|
||||||
)
|
)
|
||||||
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)
|
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, TypedDict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import Order
|
from frappe.query_builder import Order
|
||||||
from frappe.query_builder.functions import Coalesce, CombineDatetime
|
from frappe.query_builder.functions import Coalesce
|
||||||
from frappe.utils import add_days, cint, date_diff, flt, getdate
|
from frappe.utils import add_days, cint, date_diff, flt, getdate
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ class StockBalanceReport(object):
|
|||||||
item_table.item_name,
|
item_table.item_name,
|
||||||
)
|
)
|
||||||
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
|
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
.orderby(sle.posting_datetime)
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
.orderby(sle.actual_qty)
|
.orderby(sle.actual_qty)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select(
|
.select(
|
||||||
sle.item_code,
|
sle.item_code,
|
||||||
CombineDatetime(sle.posting_date, sle.posting_time).as_("date"),
|
sle.posting_datetime.as_("date"),
|
||||||
sle.warehouse,
|
sle.warehouse,
|
||||||
sle.posting_date,
|
sle.posting_date,
|
||||||
sle.posting_time,
|
sle.posting_time,
|
||||||
|
|||||||
@@ -7,13 +7,30 @@ from typing import Optional, Set, Tuple
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
<<<<<<< HEAD
|
||||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
|
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
|
||||||
|
=======
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import (
|
||||||
|
add_to_date,
|
||||||
|
cint,
|
||||||
|
cstr,
|
||||||
|
flt,
|
||||||
|
get_link_to_form,
|
||||||
|
getdate,
|
||||||
|
now,
|
||||||
|
nowdate,
|
||||||
|
nowtime,
|
||||||
|
parse_json,
|
||||||
|
)
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.utils import (
|
from erpnext.stock.utils import (
|
||||||
|
get_combine_datetime,
|
||||||
get_incoming_outgoing_rate_for_cancel,
|
get_incoming_outgoing_rate_for_cancel,
|
||||||
get_incoming_rate,
|
get_incoming_rate,
|
||||||
get_or_make_bin,
|
get_or_make_bin,
|
||||||
@@ -68,7 +85,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
|
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
|
||||||
|
|
||||||
args = sle_doc.as_dict()
|
args = sle_doc.as_dict()
|
||||||
|
<<<<<<< HEAD
|
||||||
args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False
|
args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False
|
||||||
|
=======
|
||||||
|
args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
|
|
||||||
if sle.get("voucher_type") == "Stock Reconciliation":
|
if sle.get("voucher_type") == "Stock Reconciliation":
|
||||||
# preserve previous_qty_after_transaction for qty reposting
|
# preserve previous_qty_after_transaction for qty reposting
|
||||||
@@ -431,12 +452,14 @@ class update_entries_after(object):
|
|||||||
self.process_sle(sle)
|
self.process_sle(sle)
|
||||||
|
|
||||||
def get_sle_against_current_voucher(self):
|
def get_sle_against_current_voucher(self):
|
||||||
self.args["time_format"] = "%H:%i:%s"
|
self.args["posting_datetime"] = get_combine_datetime(
|
||||||
|
self.args.posting_date, self.args.posting_time
|
||||||
|
)
|
||||||
|
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
*, timestamp(posting_date, posting_time) as "timestamp"
|
*, posting_datetime as "timestamp"
|
||||||
from
|
from
|
||||||
`tabStock Ledger Entry`
|
`tabStock Ledger Entry`
|
||||||
where
|
where
|
||||||
@@ -444,11 +467,10 @@ class update_entries_after(object):
|
|||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
and (
|
and (
|
||||||
posting_date = %(posting_date)s and
|
posting_datetime = %(posting_datetime)s
|
||||||
time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
|
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
creation ASC
|
posting_datetime ASC, creation ASC
|
||||||
for update
|
for update
|
||||||
""",
|
""",
|
||||||
self.args,
|
self.args,
|
||||||
@@ -1188,9 +1210,14 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
|||||||
|
|
||||||
args["time_format"] = "%H:%i:%s"
|
args["time_format"] = "%H:%i:%s"
|
||||||
if not args.get("posting_date"):
|
if not args.get("posting_date"):
|
||||||
args["posting_date"] = "1900-01-01"
|
args["posting_datetime"] = "1900-01-01 00:00:00"
|
||||||
if not args.get("posting_time"):
|
|
||||||
args["posting_time"] = "00:00"
|
if not args.get("posting_datetime"):
|
||||||
|
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||||
|
|
||||||
|
if operator == "<=":
|
||||||
|
# Add 1 second to handle millisecond for less than and equal to condition
|
||||||
|
args["posting_datetime"] = add_to_date(args["posting_datetime"], seconds=1)
|
||||||
|
|
||||||
voucher_condition = ""
|
voucher_condition = ""
|
||||||
if exclude_current_voucher:
|
if exclude_current_voucher:
|
||||||
@@ -1199,23 +1226,20 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
|||||||
|
|
||||||
sle = frappe.db.sql(
|
sle = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select *, timestamp(posting_date, posting_time) as "timestamp"
|
select *, posting_datetime as "timestamp"
|
||||||
from `tabStock Ledger Entry`
|
from `tabStock Ledger Entry`
|
||||||
where item_code = %(item_code)s
|
where item_code = %(item_code)s
|
||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
{voucher_condition}
|
{voucher_condition}
|
||||||
and (
|
and (
|
||||||
posting_date < %(posting_date)s or
|
posting_datetime {operator} %(posting_datetime)s
|
||||||
(
|
|
||||||
posting_date = %(posting_date)s and
|
|
||||||
time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
order by timestamp(posting_date, posting_time) desc, creation desc
|
order by posting_datetime desc, creation desc
|
||||||
limit 1
|
limit 1
|
||||||
for update""".format(
|
for update""".format(
|
||||||
operator=operator, voucher_condition=voucher_condition
|
operator=operator,
|
||||||
|
voucher_condition=voucher_condition,
|
||||||
),
|
),
|
||||||
args,
|
args,
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
@@ -1256,9 +1280,7 @@ def get_stock_ledger_entries(
|
|||||||
extra_cond=None,
|
extra_cond=None,
|
||||||
):
|
):
|
||||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||||
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
|
conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
|
||||||
operator
|
|
||||||
)
|
|
||||||
if previous_sle.get("warehouse"):
|
if previous_sle.get("warehouse"):
|
||||||
conditions += " and warehouse = %(warehouse)s"
|
conditions += " and warehouse = %(warehouse)s"
|
||||||
elif previous_sle.get("warehouse_condition"):
|
elif previous_sle.get("warehouse_condition"):
|
||||||
@@ -1284,9 +1306,11 @@ def get_stock_ledger_entries(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not previous_sle.get("posting_date"):
|
if not previous_sle.get("posting_date"):
|
||||||
previous_sle["posting_date"] = "1900-01-01"
|
previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
|
||||||
if not previous_sle.get("posting_time"):
|
else:
|
||||||
previous_sle["posting_time"] = "00:00"
|
previous_sle["posting_datetime"] = get_combine_datetime(
|
||||||
|
previous_sle["posting_date"], previous_sle["posting_time"]
|
||||||
|
)
|
||||||
|
|
||||||
if operator in (">", "<=") and previous_sle.get("name"):
|
if operator in (">", "<=") and previous_sle.get("name"):
|
||||||
conditions += " and name!=%(name)s"
|
conditions += " and name!=%(name)s"
|
||||||
@@ -1299,12 +1323,12 @@ def get_stock_ledger_entries(
|
|||||||
|
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select *, timestamp(posting_date, posting_time) as "timestamp"
|
select *, posting_datetime as "timestamp"
|
||||||
from `tabStock Ledger Entry`
|
from `tabStock Ledger Entry`
|
||||||
where item_code = %%(item_code)s
|
where item_code = %%(item_code)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
%(conditions)s
|
%(conditions)s
|
||||||
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
|
order by posting_datetime %(order)s, creation %(order)s
|
||||||
%(limit)s %(for_update)s"""
|
%(limit)s %(for_update)s"""
|
||||||
% {
|
% {
|
||||||
"conditions": conditions,
|
"conditions": conditions,
|
||||||
@@ -1330,7 +1354,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
|||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
"timestamp(posting_date, posting_time) as timestamp",
|
"posting_datetime as timestamp",
|
||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
@@ -1342,13 +1366,10 @@ def get_batch_incoming_rate(
|
|||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
|
||||||
posting_date, posting_time
|
|
||||||
)
|
|
||||||
if creation:
|
if creation:
|
||||||
timestamp_condition |= (
|
timestamp_condition |= (
|
||||||
CombineDatetime(sle.posting_date, sle.posting_time)
|
sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
|
||||||
== CombineDatetime(posting_date, posting_time)
|
|
||||||
) & (sle.creation < creation)
|
) & (sle.creation < creation)
|
||||||
|
|
||||||
batch_details = (
|
batch_details = (
|
||||||
@@ -1401,6 +1422,7 @@ def get_valuation_rate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Get valuation rate from last sle for the same item and warehouse
|
# Get valuation rate from last sle for the same item and warehouse
|
||||||
|
<<<<<<< HEAD
|
||||||
if not last_valuation_rate or last_valuation_rate[0][0] is None:
|
if not last_valuation_rate or last_valuation_rate[0][0] is None:
|
||||||
last_valuation_rate = frappe.db.sql(
|
last_valuation_rate = frappe.db.sql(
|
||||||
"""select valuation_rate
|
"""select valuation_rate
|
||||||
@@ -1416,6 +1438,20 @@ def get_valuation_rate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if last_valuation_rate:
|
if last_valuation_rate:
|
||||||
|
=======
|
||||||
|
if last_valuation_rate := frappe.db.sql(
|
||||||
|
"""select valuation_rate
|
||||||
|
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||||
|
where
|
||||||
|
item_code = %s
|
||||||
|
AND warehouse = %s
|
||||||
|
AND valuation_rate >= 0
|
||||||
|
AND is_cancelled = 0
|
||||||
|
AND NOT (voucher_no = %s AND voucher_type = %s)
|
||||||
|
order by posting_datetime desc, name desc limit 1""",
|
||||||
|
(item_code, warehouse, voucher_no, voucher_type),
|
||||||
|
):
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
return flt(last_valuation_rate[0][0])
|
return flt(last_valuation_rate[0][0])
|
||||||
|
|
||||||
# If negative stock allowed, and item delivered without any incoming entry,
|
# If negative stock allowed, and item delivered without any incoming entry,
|
||||||
@@ -1473,6 +1509,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
qty_shift = args.actual_qty
|
qty_shift = args.actual_qty
|
||||||
|
|
||||||
args["time_format"] = "%H:%i:%s"
|
args["time_format"] = "%H:%i:%s"
|
||||||
|
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||||
|
|
||||||
# find difference/shift in qty caused by stock reconciliation
|
# find difference/shift in qty caused by stock reconciliation
|
||||||
if args.voucher_type == "Stock Reconciliation":
|
if args.voucher_type == "Stock Reconciliation":
|
||||||
@@ -1482,8 +1519,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
next_stock_reco_detail = get_next_stock_reco(args)
|
next_stock_reco_detail = get_next_stock_reco(args)
|
||||||
if next_stock_reco_detail:
|
if next_stock_reco_detail:
|
||||||
detail = next_stock_reco_detail[0]
|
detail = next_stock_reco_detail[0]
|
||||||
|
|
||||||
# add condition to update SLEs before this date & time
|
|
||||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||||
|
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
@@ -1496,13 +1531,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
and voucher_no != %(voucher_no)s
|
and voucher_no != %(voucher_no)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
and (
|
and (
|
||||||
posting_date > %(posting_date)s or
|
posting_datetime > %(posting_datetime)s
|
||||||
(
|
|
||||||
posting_date = %(posting_date)s and
|
|
||||||
time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
{datetime_limit_condition}
|
{datetime_limit_condition}
|
||||||
""",
|
""",
|
||||||
args,
|
args,
|
||||||
)
|
)
|
||||||
@@ -1557,20 +1588,11 @@ def get_next_stock_reco(kwargs):
|
|||||||
& (sle.voucher_no != kwargs.get("voucher_no"))
|
& (sle.voucher_no != kwargs.get("voucher_no"))
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
& (
|
& (
|
||||||
(
|
sle.posting_datetime
|
||||||
CombineDatetime(sle.posting_date, sle.posting_time)
|
>= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||||
> CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
|
||||||
)
|
|
||||||
| (
|
|
||||||
(
|
|
||||||
CombineDatetime(sle.posting_date, sle.posting_time)
|
|
||||||
== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
|
||||||
)
|
|
||||||
& (sle.creation > kwargs.get("creation"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
.orderby(sle.posting_datetime)
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
@@ -1582,11 +1604,13 @@ def get_next_stock_reco(kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def get_datetime_limit_condition(detail):
|
def get_datetime_limit_condition(detail):
|
||||||
|
posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
and
|
and
|
||||||
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
|
(posting_datetime < '{posting_datetime}'
|
||||||
or (
|
or (
|
||||||
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
|
posting_datetime = '{posting_datetime}'
|
||||||
and creation < '{detail.creation}'
|
and creation < '{detail.creation}'
|
||||||
)
|
)
|
||||||
)"""
|
)"""
|
||||||
@@ -1648,6 +1672,7 @@ def is_negative_with_precision(neg_sle, is_batch=False):
|
|||||||
return qty_deficit < 0 and abs(qty_deficit) > 0.0001
|
return qty_deficit < 0 and abs(qty_deficit) > 0.0001
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
def get_future_sle_with_negative_qty(sle):
|
def get_future_sle_with_negative_qty(sle):
|
||||||
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
query = (
|
query = (
|
||||||
@@ -1668,6 +1693,27 @@ def get_future_sle_with_negative_qty(sle):
|
|||||||
)
|
)
|
||||||
.orderby(CombineDatetime(SLE.posting_date, SLE.posting_time))
|
.orderby(CombineDatetime(SLE.posting_date, SLE.posting_time))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
=======
|
||||||
|
def get_future_sle_with_negative_qty(args):
|
||||||
|
return frappe.db.sql(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
qty_after_transaction, posting_date, posting_time,
|
||||||
|
voucher_type, voucher_no
|
||||||
|
from `tabStock Ledger Entry`
|
||||||
|
where
|
||||||
|
item_code = %(item_code)s
|
||||||
|
and warehouse = %(warehouse)s
|
||||||
|
and voucher_no != %(voucher_no)s
|
||||||
|
and posting_datetime >= %(posting_datetime)s
|
||||||
|
and is_cancelled = 0
|
||||||
|
and qty_after_transaction < 0
|
||||||
|
order by posting_datetime asc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
args,
|
||||||
|
as_dict=1,
|
||||||
|
>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries)
|
||||||
)
|
)
|
||||||
|
|
||||||
if sle.voucher_type == "Stock Reconciliation" and sle.batch_no:
|
if sle.voucher_type == "Stock Reconciliation" and sle.batch_no:
|
||||||
@@ -1681,20 +1727,20 @@ def get_future_sle_with_negative_batch_qty(args):
|
|||||||
"""
|
"""
|
||||||
with batch_ledger as (
|
with batch_ledger as (
|
||||||
select
|
select
|
||||||
posting_date, posting_time, voucher_type, voucher_no,
|
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
|
||||||
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
|
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
|
||||||
from `tabStock Ledger Entry`
|
from `tabStock Ledger Entry`
|
||||||
where
|
where
|
||||||
item_code = %(item_code)s
|
item_code = %(item_code)s
|
||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and batch_no=%(batch_no)s
|
and batch_no=%(batch_no)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
order by posting_date, posting_time, creation
|
order by posting_datetime, creation
|
||||||
)
|
)
|
||||||
select * from batch_ledger
|
select * from batch_ledger
|
||||||
where
|
where
|
||||||
cumulative_total < 0.0
|
cumulative_total < 0.0
|
||||||
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
|
and posting_datetime >= %(posting_datetime)s
|
||||||
limit 1
|
limit 1
|
||||||
""",
|
""",
|
||||||
args,
|
args,
|
||||||
@@ -1746,6 +1792,7 @@ def is_internal_transfer(sle):
|
|||||||
|
|
||||||
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
|
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
|
||||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
posting_datetime = get_combine_datetime(posting_date, posting_time)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
@@ -1754,10 +1801,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
|
|||||||
(table.is_cancelled == 0)
|
(table.is_cancelled == 0)
|
||||||
& (table.item_code == item_code)
|
& (table.item_code == item_code)
|
||||||
& (table.warehouse == warehouse)
|
& (table.warehouse == warehouse)
|
||||||
& (
|
& (table.posting_datetime <= posting_datetime)
|
||||||
(table.posting_date < posting_date)
|
|
||||||
| ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Dict, Optional
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
||||||
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
|
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||||
@@ -619,3 +619,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option
|
|||||||
):
|
):
|
||||||
scan_result.update(item_info)
|
scan_result.update(item_info)
|
||||||
return scan_result
|
return scan_result
|
||||||
|
|
||||||
|
|
||||||
|
def get_combine_datetime(posting_date, posting_time):
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
if isinstance(posting_date, str):
|
||||||
|
posting_date = getdate(posting_date)
|
||||||
|
|
||||||
|
if isinstance(posting_time, str):
|
||||||
|
posting_time = get_time(posting_time)
|
||||||
|
|
||||||
|
if isinstance(posting_time, datetime.timedelta):
|
||||||
|
posting_time = (datetime.datetime.min + posting_time).time()
|
||||||
|
|
||||||
|
return datetime.datetime.combine(posting_date, posting_time)
|
||||||
|
|||||||
Reference in New Issue
Block a user