diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 85624d5afb..b55574fb4a 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -38,6 +38,7 @@ class BuyingController(SubcontractingController):
self.set_supplier_address()
self.validate_asset_return()
self.validate_auto_repeat_subscription_dates()
+ self.create_package_for_transfer()
if self.doctype == "Purchase Invoice":
self.validate_purchase_receipt_if_update_stock()
@@ -69,6 +70,36 @@ class BuyingController(SubcontractingController):
),
)
+ def create_package_for_transfer(self) -> None:
+ """Create serial and batch package for Sourece Warehouse in case of inter transfer."""
+
+ if self.is_internal_transfer() and (
+ self.doctype == "Purchase Receipt" or (self.doctype == "Purchase Invoice" and self.update_stock)
+ ):
+ field = "delivery_note_item" if self.doctype == "Purchase Receipt" else "sales_invoice_item"
+
+ doctype = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
+
+ ids = [d.get(field) for d in self.get("items") if d.get(field)]
+ bundle_ids = {}
+ if ids:
+ for bundle in frappe.get_all(
+ doctype, filters={"name": ("in", ids)}, fields=["serial_and_batch_bundle", "name"]
+ ):
+ bundle_ids[bundle.name] = bundle.serial_and_batch_bundle
+
+ if not bundle_ids:
+ return
+
+ for item in self.get("items"):
+ if item.get(field) and not item.serial_and_batch_bundle:
+ item.serial_and_batch_bundle = self.make_package_for_transfer(
+ bundle_ids.get(item.get(field)),
+ item.from_warehouse,
+ type_of_transaction="Outward",
+ do_not_submit=True,
+ )
+
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -467,7 +498,11 @@ class BuyingController(SubcontractingController):
{
"actual_qty": flt(pr_qty),
"serial_no": cstr(d.serial_no).strip(),
- "serial_and_batch_bundle": d.serial_and_batch_bundle,
+ "serial_and_batch_bundle": (
+ d.serial_and_batch_bundle
+ if not self.is_internal_transfer()
+ else self.get_package_for_target_warehouse(d)
+ ),
},
)
@@ -494,7 +529,6 @@ class BuyingController(SubcontractingController):
"recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
else 0,
- "serial_and_batch_bundle": d.serial_and_batch_bundle,
}
)
sl_entries.append(sle)
@@ -531,6 +565,15 @@ class BuyingController(SubcontractingController):
via_landed_cost_voucher=via_landed_cost_voucher,
)
+ def get_package_for_target_warehouse(self, item) -> str:
+ if not item.serial_and_batch_bundle:
+ return ""
+
+ return self.make_package_for_transfer(
+ item.serial_and_batch_bundle,
+ item.warehouse,
+ )
+
def update_ordered_and_reserved_qty(self):
po_map = {}
for d in self.get("items"):
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 15c84a96c8..1dd7209b16 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -531,6 +531,11 @@ class SellingController(StockController):
if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name
+ if item_row.serial_and_batch_bundle:
+ sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
+ item_row.serial_and_batch_bundle, item_row.target_warehouse
+ )
+
return sle
def set_po_nos(self, for_validate=False):
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2e705eaf2c..2048a42323 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -372,6 +372,44 @@ class StockController(AccountsController):
row.db_set("serial_and_batch_bundle", None)
+ def make_package_for_transfer(
+ self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
+ ):
+ bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
+
+ if not type_of_transaction:
+ type_of_transaction = "Inward"
+
+ bundle_doc = frappe.copy_doc(bundle_doc)
+ bundle_doc.warehouse = warehouse
+ bundle_doc.type_of_transaction = type_of_transaction
+ bundle_doc.voucher_type = self.doctype
+ bundle_doc.voucher_no = self.name
+ bundle_doc.is_cancelled = 0
+
+ for row in bundle_doc.ledgers:
+ row.is_outward = 0
+ row.qty = abs(row.qty)
+ row.stock_value_difference = abs(row.stock_value_difference)
+ if type_of_transaction == "Outward":
+ row.qty *= -1
+ row.stock_value_difference *= row.stock_value_difference
+ row.is_outward = 1
+
+ row.warehouse = warehouse
+
+ bundle_doc.set_total_qty()
+ bundle_doc.set_avg_rate()
+ bundle_doc.flags.ignore_permissions = True
+
+ if not do_not_submit:
+ bundle_doc.submit()
+ else:
+ bundle_doc.save(ignore_permissions=True)
+
+ print(bundle_doc.name)
+ return bundle_doc.name
+
def get_sl_entries(self, d, args):
sl_dict = frappe._dict(
{
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 33b89553b8..f2d266afbc 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -69,7 +69,8 @@ class DeprecatedBatchNoValuation:
def calculate_avg_rate_from_deprecarated_ledgers(self):
ledgers = self.get_sle_for_batches()
for ledger in ledgers:
- self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
+ self.batch_avg_rate[ledger.batch_no] += flt(ledger.batch_value) / flt(ledger.batch_qty)
+ self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
def get_sle_for_batches(self):
batch_nos = list(self.batch_nos.keys())
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index a647a17f80..ce0684a69b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -1044,8 +1044,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"field_map": {
source_document_warehouse_field: target_document_warehouse_field,
"name": "delivery_note_item",
- "batch_no": "batch_no",
- "serial_no": "serial_no",
"purchase_order": "purchase_order",
"purchase_order_item": "purchase_order_item",
"material_request": "material_request",
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 98da0afdee..824691cafc 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
@@ -7,19 +7,24 @@ from typing import Dict, List
import frappe
from frappe import _, bold
from frappe.model.document import Document
-from frappe.query_builder.functions import Sum
-from frappe.utils import cint, flt, today
+from frappe.query_builder.functions import CombineDatetime, Sum
+from frappe.utils import cint, flt, get_link_to_form, today
from pypika import Case
from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+class SerialNoExistsInFutureTransactionError(frappe.ValidationError):
+ pass
+
+
class SerialandBatchBundle(Document):
def validate(self):
self.validate_serial_and_batch_no()
self.validate_duplicate_serial_and_batch_no()
# self.validate_voucher_no()
- self.validate_serial_nos()
+ self.check_future_entries_exists()
+ self.validate_serial_nos_inventory()
def before_save(self):
self.set_total_qty()
@@ -31,6 +36,26 @@ class SerialandBatchBundle(Document):
if self.ledgers:
self.set_avg_rate()
+ def validate_serial_nos_inventory(self):
+ if not (self.has_serial_no and self.type_of_transaction == "Outward"):
+ return
+
+ serial_nos = [d.serial_no for d in self.ledgers if d.serial_no]
+ serial_no_warehouse = frappe._dict(
+ frappe.get_all(
+ "Serial No",
+ filters={"name": ("in", serial_nos)},
+ fields=["name", "warehouse"],
+ as_list=1,
+ )
+ )
+
+ for serial_no in serial_nos:
+ if serial_no_warehouse.get(serial_no) != self.warehouse:
+ frappe.throw(
+ _(f"Serial No {bold(serial_no)} is not present in the warehouse {bold(self.warehouse)}.")
+ )
+
def set_incoming_rate(self, row=None, save=False):
if self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(row, save)
@@ -65,10 +90,14 @@ class SerialandBatchBundle(Document):
)
for d in self.ledgers:
+ available_qty = 0
if self.has_serial_no:
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
else:
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
+ available_qty = sn_obj.batch_available_qty.get(d.batch_no) + d.qty
+
+ self.validate_negative_batch(d.batch_no, available_qty)
if self.has_batch_no:
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -78,6 +107,14 @@ class SerialandBatchBundle(Document):
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
)
+ def validate_negative_batch(self, batch_no, available_qty):
+ if available_qty < 0:
+ msg = f"""Batch No {bold(batch_no)} has negative stock
+ of quantity {bold(available_qty)} in the
+ warehouse {self.warehouse}"""
+
+ frappe.throw(_(msg))
+
def get_sle_for_outward_transaction(self, row):
return frappe._dict(
{
@@ -169,10 +206,54 @@ class SerialandBatchBundle(Document):
)
)
- def validate_serial_nos(self):
+ def check_future_entries_exists(self):
if not self.has_serial_no:
return
+ serial_nos = [d.serial_no for d in self.ledgers if d.serial_no]
+
+ parent = frappe.qb.DocType("Serial and Batch Bundle")
+ child = frappe.qb.DocType("Serial and Batch Ledger")
+
+ timestamp_condition = CombineDatetime(
+ parent.posting_date, parent.posting_time
+ ) > CombineDatetime(self.posting_date, self.posting_time)
+
+ future_entries = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(
+ child.serial_no,
+ parent.voucher_type,
+ parent.voucher_no,
+ )
+ .where(
+ (child.serial_no.isin(serial_nos))
+ & (child.parent != self.name)
+ & (parent.item_code == self.item_code)
+ & (parent.docstatus == 1)
+ & (parent.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ ).run(as_dict=True)
+
+ if future_entries:
+ msg = """The serial nos has been used in the future
+ transactions so you need to cancel them first.
+ The list of serial nos and their respective
+ transactions are as below."""
+
+ msg += "