refactor: serial and batch reposting
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 3f77869..c75d57f 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -70,6 +70,7 @@
"target_warehouse",
"quality_inspection",
"col_break4",
+ "allow_zero_valuation_rate",
"against_sales_order",
"so_detail",
"against_sales_invoice",
@@ -79,6 +80,10 @@
"section_break_40",
"pick_serial_and_batch",
"serial_and_batch_bundle",
+ "column_break_eaoe",
+ "serial_no",
+ "batch_no",
+ "available_qty_section",
"actual_batch_qty",
"actual_qty",
"installed_qty",
@@ -88,7 +93,6 @@
"received_qty",
"accounting_details_section",
"expense_account",
- "allow_zero_valuation_rate",
"column_break_71",
"internal_transfer_section",
"material_request",
@@ -505,7 +509,8 @@
},
{
"fieldname": "section_break_40",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Serial and Batch No"
},
{
"allow_on_submit": 1,
@@ -847,19 +852,44 @@
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
- "options": "Serial and Batch Bundle"
+ "no_copy": 1,
+ "options": "Serial and Batch Bundle",
+ "print_hide": 1
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "available_qty_section",
+ "fieldtype": "Section Break",
+ "label": "Available Qty"
+ },
+ {
+ "fieldname": "column_break_eaoe",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Text",
+ "label": "Serial No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-05-01 21:05:14.175640",
+ "modified": "2023-05-02 21:05:14.175640",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 900fb75..f779893 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -79,6 +79,7 @@
"purchase_order",
"purchase_invoice",
"column_break_40",
+ "allow_zero_valuation_rate",
"is_fixed_asset",
"asset_location",
"asset_category",
@@ -93,8 +94,12 @@
"section_break_45",
"update_serial_batch_bundle",
"serial_and_batch_bundle",
+ "rejected_serial_and_batch_bundle",
"col_break5",
- "allow_zero_valuation_rate",
+ "serial_no",
+ "rejected_serial_no",
+ "batch_no",
+ "subcontract_bom_section",
"include_exploded_items",
"bom",
"item_weight_details",
@@ -998,12 +1003,43 @@
"fieldname": "update_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
+ },
+ {
+ "depends_on": "eval:parent.is_old_subcontracting_flow",
+ "fieldname": "subcontract_bom_section",
+ "fieldtype": "Section Break",
+ "label": "Subcontract BOM"
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Text",
+ "label": "Serial No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rejected_serial_no",
+ "fieldtype": "Text",
+ "label": "Rejected Serial No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rejected_serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": "Rejected Serial and Batch Bundle",
+ "options": "Serial and Batch Bundle"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-02-28 16:43:04.470104",
+ "modified": "2023-03-03 12:45:03.087766",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
index cfe35d7..4148946 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -7,8 +7,8 @@
"field_order": [
"item_details_tab",
"company",
- "item_group",
"warehouse",
+ "type_of_transaction",
"column_break_4",
"item_code",
"item_name",
@@ -18,6 +18,7 @@
"ledgers",
"quantity_and_rate_section",
"total_qty",
+ "item_group",
"column_break_13",
"avg_rate",
"total_amount",
@@ -46,6 +47,7 @@
"fetch_from": "item_code.item_group",
"fieldname": "item_group",
"fieldtype": "Link",
+ "hidden": 1,
"label": "Item Group",
"options": "Item Group"
},
@@ -171,12 +173,19 @@
"label": "Warehouse",
"options": "Warehouse",
"reqd": 1
+ },
+ {
+ "fieldname": "type_of_transaction",
+ "fieldtype": "Select",
+ "label": "Type of Transaction",
+ "options": "\nInward\nOutward",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-10 11:32:09.018760",
+ "modified": "2023-03-03 16:18:53.709069",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 1c9dc15..0f8f6d2 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -267,7 +267,11 @@
serial_batch_table.qty,
serial_batch_table.incoming_rate,
)
- .where((sle_table.item_code == kwargs.item_code) & (sle_table.warehouse == kwargs.warehouse))
+ .where(
+ (sle_table.item_code == kwargs.item_code)
+ & (sle_table.warehouse == kwargs.warehouse)
+ & (serial_batch_table.is_outward == 0)
+ )
)
if kwargs.serial_nos:
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
index 65eaa03..d993225 100644
--- a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
@@ -15,7 +15,8 @@
"incoming_rate",
"column_break_8",
"outgoing_rate",
- "stock_value_difference"
+ "stock_value_difference",
+ "is_outward"
],
"fields": [
{
@@ -93,12 +94,19 @@
"label": "Change in Stock Value",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_outward",
+ "fieldtype": "Check",
+ "label": "Is Outward",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-01-10 12:55:57.368650",
+ "modified": "2023-03-03 16:52:26.039613",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Ledger",
diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py
diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js
new file mode 100644
index 0000000..c36abd6
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Serial and Batch No Bundle", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json
new file mode 100644
index 0000000..ec33156
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json
@@ -0,0 +1,176 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:56:38.338267",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_details_tab",
+ "company",
+ "item_group",
+ "has_serial_no",
+ "column_break_4",
+ "item_code",
+ "item_name",
+ "has_batch_no",
+ "serial_no_and_batch_no_tab",
+ "ledgers",
+ "qty",
+ "reference_tab",
+ "voucher_type",
+ "voucher_no",
+ "posting_date",
+ "posting_time",
+ "is_cancelled",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "item_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_serial_no",
+ "fieldname": "has_serial_no",
+ "fieldtype": "Check",
+ "label": "Has Serial No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_batch_no",
+ "fieldname": "has_batch_no",
+ "fieldtype": "Check",
+ "label": "Has Batch No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "serial_no_and_batch_no_tab",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "ledgers",
+ "fieldtype": "Table",
+ "label": "Serial No and Batch No Transaction",
+ "options": "Serial and Batch No Ledger",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Total Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reference_tab",
+ "fieldtype": "Tab Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_cancelled",
+ "fieldtype": "Check",
+ "label": "Is Cancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Serial and Batch No Bundle",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-03-05 17:38:51.871723",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Serial and Batch No Bundle",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "item_code"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py
new file mode 100644
index 0000000..46c0e5a
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SerialandBatchNoBundle(Document):
+ pass
diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py
new file mode 100644
index 0000000..2d5b9d3
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestSerialandBatchNoBundle(FrappeTestCase):
+ pass
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
new file mode 100644
index 0000000..f32b79d
--- /dev/null
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -0,0 +1,385 @@
+import frappe
+from frappe.model.naming import make_autoname
+from frappe.query_builder.functions import CombineDatetime, Sum
+from frappe.utils import cint, cstr, flt, now
+
+from erpnext.stock.valuation import round_off_if_near_zero
+
+
+class SerialBatchBundle:
+ def __init__(self, **kwargs):
+ for key, value in kwargs.iteritems():
+ setattr(self, key, value)
+
+ self.set_item_details()
+
+ def process_serial_and_batch_bundle(self):
+ if self.item_details.has_serial_no:
+ self.process_serial_no
+ elif self.item_details.has_batch_no:
+ self.process_batch_no
+
+ def set_item_details(self):
+ fields = [
+ "has_batch_no",
+ "has_serial_no",
+ "item_name",
+ "item_group",
+ "serial_no_series",
+ "create_new_batch",
+ "batch_number_series",
+ ]
+
+ self.item_details = frappe.get_cached_value("Item", self.sle.item_code, fields, as_dict=1)
+
+ def process_serial_no(self):
+ if (
+ not self.sle.is_cancelled
+ and not self.sle.serial_and_batch_bundle
+ and self.sle.actual_qty > 0
+ and self.item_details.has_serial_no == 1
+ and self.item_details.serial_no_series
+ ):
+ sr_nos = self.auto_create_serial_nos()
+ self.make_serial_no_bundle(sr_nos)
+
+ def auto_create_serial_nos(self):
+ sr_nos = []
+ serial_nos_details = []
+
+ for i in range(cint(self.sle.actual_qty)):
+ serial_no = make_autoname(self.item_details.serial_no_series, "Serial No")
+ sr_nos.append(serial_no)
+ serial_nos_details.append(
+ (
+ serial_no,
+ serial_no,
+ now(),
+ now(),
+ frappe.session.user,
+ frappe.session.user,
+ self.warehouse,
+ self.company,
+ self.item_code,
+ self.item_details.item_name,
+ self.item_details.description,
+ )
+ )
+
+ if serial_nos_details:
+ fields = [
+ "name",
+ "serial_no",
+ "creation",
+ "modified",
+ "owner",
+ "modified_by",
+ "warehouse",
+ "company",
+ "item_code",
+ "item_name",
+ "description",
+ ]
+
+ frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
+ return sr_nos
+
+ def make_serial_no_bundle(self, serial_nos=None):
+ sn_doc = frappe.new_doc("Serial and Batch Bundle")
+ sn_doc.item_code = self.item_code
+ sn_doc.item_name = self.item_details.item_name
+ sn_doc.item_group = self.item_details.item_group
+ sn_doc.has_serial_no = self.item_details.has_serial_no
+ sn_doc.has_batch_no = self.item_details.has_batch_no
+ sn_doc.voucher_type = self.sle.voucher_type
+ sn_doc.voucher_no = self.sle.voucher_no
+ sn_doc.flags.ignore_mandatory = True
+ sn_doc.flags.ignore_validate = True
+ sn_doc.total_qty = self.sle.actual_qty
+ sn_doc.avg_rate = self.sle.incoming_rate
+ sn_doc.total_amount = flt(self.sle.actual_qty) * flt(self.sle.incoming_rate)
+ sn_doc.insert()
+
+ batch_no = ""
+ if self.item_details.has_batch_no:
+ batch_no = self.create_batch()
+
+ if serial_nos:
+ self.add_serial_no_to_bundle(sn_doc, serial_nos, batch_no)
+ elif self.item_details.has_batch_no:
+ self.add_batch_no_to_bundle(sn_doc, batch_no)
+ sn_doc.save()
+
+ sn_doc.load_from_db()
+ sn_doc.flags.ignore_validate = True
+ sn_doc.flags.ignore_mandatory = True
+
+ sn_doc.submit()
+
+ self.sle.serial_and_batch_bundle = sn_doc.name
+
+ def add_serial_no_to_bundle(self, sn_doc, serial_nos, batch_no=None):
+ ledgers = []
+
+ fields = [
+ "name",
+ "serial_no",
+ "batch_no",
+ "warehouse",
+ "item_code",
+ "qty",
+ "incoming_rate",
+ "parent",
+ "parenttype",
+ "parentfield",
+ ]
+
+ for serial_no in serial_nos:
+ ledgers.append(
+ (
+ frappe.generate_hash("Serial and Batch Ledger", 10),
+ serial_no,
+ batch_no,
+ self.warehouse,
+ self.item_details.item_code,
+ 1,
+ self.sle.incoming_rate,
+ sn_doc.name,
+ sn_doc.doctype,
+ "ledgers",
+ )
+ )
+
+ frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers))
+
+ def add_batch_no_to_bundle(self, sn_doc, batch_no):
+ sn_doc.append(
+ "ledgers",
+ {
+ "batch_no": batch_no,
+ "qty": self.sle.actual_qty,
+ "incoming_rate": self.sle.incoming_rate,
+ },
+ )
+
+ def create_batch(self):
+ from erpnext.stock.doctype.batch.batch import make_batch
+
+ return make_batch(
+ frappe._dict(
+ {
+ "item": self.item_code,
+ "reference_doctype": self.sle.voucher_type,
+ "reference_name": self.sle.voucher_no,
+ }
+ )
+ )
+
+ def process_batch_no(self):
+ if (
+ not self.sle.is_cancelled
+ and not self.sle.serial_and_batch_bundle
+ and self.sle.actual_qty > 0
+ and self.item_details.has_batch_no == 1
+ and self.item_details.create_new_batch
+ and self.item_details.batch_number_series
+ ):
+ self.make_serial_no_bundle()
+
+
+class RepostSerialBatchBundle:
+ def __init__(self, **kwargs):
+ for key, value in kwargs.iteritems():
+ setattr(self, key, value)
+
+ def get_valuation_rate(self):
+ if self.sle.actual_qty > 0:
+ self.sle.incoming_rate = self.sle.valuation_rate
+
+ if self.sle.actual_qty < 0:
+ self.sle.outgoing_rate = self.sle.valuation_rate
+
+ def get_valuation_rate_for_serial_nos(self):
+ serial_nos = self.get_serial_nos()
+
+ subquery = f"""
+ SELECT
+ MAX(ledger.posting_date), name
+ FROM
+ ledger
+ WHERE
+ ledger.serial_no IN {tuple(serial_nos)}
+ AND ledger.is_outward = 0
+ AND ledger.warehouse = {frappe.db.escape(self.sle.warehouse)}
+ AND ledger.item_code = {frappe.db.escape(self.sle.item_code)}
+ AND (
+ ledger.posting_date < '{self.sle.posting_date}'
+ OR (
+ ledger.posting_date = '{self.sle.posting_date}'
+ AND ledger.posting_time <= '{self.sle.posting_time}'
+ )
+ )
+ """
+
+ frappe.db.sql(
+ """
+ SELECT
+ serial_no, incoming_rate
+ FROM
+ `tabSerial and Batch Ledger` AS ledger,
+ ({subquery}) AS SubQuery
+ WHERE
+ ledger.name = SubQuery.name
+ GROUP BY
+ ledger.serial_no
+ """
+ )
+
+ def get_serial_nos(self):
+ ledgers = frappe.get_all(
+ "Serial and Batch Ledger",
+ fields=["serial_no"],
+ filters={"parent": self.sle.serial_and_batch_bundle, "is_outward": 1},
+ )
+
+ return [d.serial_no for d in ledgers]
+
+
+class DeprecatedRepostSerialBatchBundle(RepostSerialBatchBundle):
+ def get_serialized_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+ serial_nos = cstr(sle.serial_no).split("\n")
+
+ if incoming_rate < 0:
+ # wrong incoming rate
+ incoming_rate = self.wh_data.valuation_rate
+
+ stock_value_change = 0
+ if actual_qty > 0:
+ stock_value_change = actual_qty * incoming_rate
+ else:
+ # In case of delivery/stock issue, get average purchase rate
+ # of serial nos of current entry
+ if not sle.is_cancelled:
+ outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
+ stock_value_change = -1 * outgoing_value
+ else:
+ stock_value_change = actual_qty * sle.outgoing_rate
+
+ new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
+
+ if new_stock_qty > 0:
+ new_stock_value = (
+ self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
+ ) + stock_value_change
+ if new_stock_value >= 0:
+ # calculate new valuation rate only if stock value is positive
+ # else it remains the same as that of previous entry
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
+
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
+ allow_zero_rate = self.check_if_allow_zero_valuation_rate(
+ sle.voucher_type, sle.voucher_detail_no
+ )
+ if not allow_zero_rate:
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
+
+ def get_incoming_value_for_serial_nos(self, sle, serial_nos):
+ # get rate from serial nos within same company
+ all_serial_nos = frappe.get_all(
+ "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
+ )
+
+ incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company)
+
+ # Get rate for serial nos which has been transferred to other company
+ invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
+ for serial_no in invalid_serial_nos:
+ incoming_rate = frappe.db.sql(
+ """
+ select incoming_rate
+ from `tabStock Ledger Entry`
+ where
+ company = %s
+ and actual_qty > 0
+ and is_cancelled = 0
+ and (serial_no = %s
+ or serial_no like %s
+ or serial_no like %s
+ or serial_no like %s
+ )
+ order by posting_date desc
+ limit 1
+ """,
+ (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
+ )
+
+ incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
+
+ return incoming_values
+
+ def update_batched_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+
+ self.wh_data.qty_after_transaction = round_off_if_near_zero(
+ self.wh_data.qty_after_transaction + actual_qty
+ )
+
+ if actual_qty > 0:
+ stock_value_difference = incoming_rate * actual_qty
+ else:
+ outgoing_rate = get_batch_incoming_rate(
+ item_code=sle.item_code,
+ warehouse=sle.warehouse,
+ batch_no=sle.batch_no,
+ posting_date=sle.posting_date,
+ posting_time=sle.posting_time,
+ creation=sle.creation,
+ )
+ if outgoing_rate is None:
+ # This can *only* happen if qty available for the batch is zero.
+ # in such case fall back various other rates.
+ # future entries will correct the overall accounting as each
+ # batch individually uses moving average rates.
+ outgoing_rate = self.get_fallback_rate(sle)
+ stock_value_difference = outgoing_rate * actual_qty
+
+ self.wh_data.stock_value = round_off_if_near_zero(
+ self.wh_data.stock_value + stock_value_difference
+ )
+ if self.wh_data.qty_after_transaction:
+ self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
+
+
+def get_batch_incoming_rate(
+ item_code, warehouse, batch_no, posting_date, posting_time, creation=None
+):
+
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+
+ timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
+ posting_date, posting_time
+ )
+ if creation:
+ timestamp_condition |= (
+ CombineDatetime(sle.posting_date, sle.posting_time)
+ == CombineDatetime(posting_date, posting_time)
+ ) & (sle.creation < creation)
+
+ batch_details = (
+ frappe.qb.from_(sle)
+ .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
+ .where(
+ (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.batch_no == batch_no)
+ & (sle.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ ).run(as_dict=True)
+
+ if batch_details and batch_details[0].batch_qty:
+ return batch_details[0].batch_value / batch_details[0].batch_qty