fix: use serial batch fields for subcontracting receipt (#40311)
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index c46ef50..a477a0d 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -439,9 +439,21 @@
filtered_batches = get_filterd_batches(batches)
+ if filters.get("is_inward"):
+ filtered_batches.extend(get_empty_batches(filters))
+
return filtered_batches
+def get_empty_batches(filters):
+ return frappe.get_all(
+ "Batch",
+ fields=["name", "batch_qty"],
+ filters={"item": filters.get("item_code"), "batch_qty": 0.0},
+ as_list=1,
+ )
+
+
def get_filterd_batches(data):
batches = OrderedDict()
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index a67fbdc..2f05ec3 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -190,43 +190,23 @@
if row.use_serial_batch_fields and (
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
- if self.doctype == "Stock Reconciliation":
- qty = row.qty
- type_of_transaction = "Inward"
- warehouse = row.warehouse
- elif table_name == "packed_items":
- qty = row.qty
- warehouse = row.warehouse
- type_of_transaction = "Outward"
- if self.is_return:
- type_of_transaction = "Inward"
- else:
- qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty
- type_of_transaction = get_type_of_transaction(self, row)
- warehouse = (
- row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse
- )
+ bundle_details = {
+ "item_code": row.item_code,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "company": self.company,
+ "is_rejected": 1 if row.get("rejected_warehouse") else 0,
+ "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
+ "batch_no": row.batch_no,
+ "use_serial_batch_fields": row.use_serial_batch_fields,
+ "do_not_submit": True,
+ }
- sn_doc = SerialBatchCreation(
- {
- "item_code": row.item_code,
- "warehouse": warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- "voucher_detail_no": row.name,
- "qty": qty,
- "type_of_transaction": type_of_transaction,
- "company": self.company,
- "is_rejected": 1 if row.get("rejected_warehouse") else 0,
- "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
- "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
- "batch_no": row.batch_no,
- "use_serial_batch_fields": row.use_serial_batch_fields,
- "do_not_submit": True,
- }
- ).make_serial_and_batch_bundle()
+ self.update_bundle_details(bundle_details, table_name, row)
+ sn_doc = SerialBatchCreation(bundle_details).make_serial_and_batch_bundle()
if sn_doc.is_rejected:
row.rejected_serial_and_batch_bundle = sn_doc.name
@@ -243,6 +223,34 @@
}
)
+ def update_bundle_details(self, bundle_details, table_name, row):
+ # Since qty field is different for different doctypes
+ qty = row.get("qty")
+ warehouse = row.get("warehouse")
+
+ if table_name == "packed_items":
+ type_of_transaction = "Inward"
+ if not self.is_return:
+ type_of_transaction = "Outward"
+ else:
+ type_of_transaction = get_type_of_transaction(self, row)
+
+ if hasattr(row, "stock_qty"):
+ qty = row.stock_qty
+
+ if self.doctype == "Stock Entry":
+ qty = row.transfer_qty
+ warehouse = row.s_warehouse or row.t_warehouse
+
+ bundle_details.update(
+ {
+ "qty": qty,
+ "type_of_transaction": type_of_transaction,
+ "warehouse": warehouse,
+ "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
+ }
+ )
+
def validate_serial_nos_and_batches_with_bundle(self, row):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index fccaf88..d0064b3 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -371,11 +371,18 @@
label: __('Batch No'),
in_list_view: 1,
get_query: () => {
+ let is_inward = false;
+ if ((["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) && !this.frm.doc.is_return)
+ || (this.frm.doc.doctype === 'Stock Entry' && this.frm.doc.purpose === 'Material Receipt')) {
+ is_inward = true;
+ }
+
return {
query : "erpnext.controllers.queries.get_batch_no",
filters: {
'item_code': this.item.item_code,
- 'warehouse': this.item.s_warehouse || this.item.t_warehouse,
+ 'warehouse': this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
+ 'is_inward': is_inward
}
}
},
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 dd59a5d..08cb3ca 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
@@ -1263,6 +1263,13 @@
if parent_doc.get("is_return"):
type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward"
+ if parent_doc.get("doctype") == "Subcontracting Receipt":
+ type_of_transaction = "Outward"
+ if child_row.get("doctype") == "Subcontracting Receipt Item":
+ type_of_transaction = "Inward"
+ elif parent_doc.get("doctype") == "Stock Reconciliation":
+ type_of_transaction = "Inward"
+
return type_of_transaction
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d8d9d68..f79803c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -10,17 +10,7 @@
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
-from frappe.utils import (
- cint,
- comma_or,
- cstr,
- flt,
- format_time,
- formatdate,
- getdate,
- month_diff,
- nowdate,
-)
+from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -237,41 +227,6 @@
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
- def submit(self):
- if self.is_enqueue_action():
- frappe.msgprint(
- _(
- "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
- )
- )
- self.queue_action("submit", timeout=2000)
- else:
- self._submit()
-
- def cancel(self):
- if self.is_enqueue_action():
- frappe.msgprint(
- _(
- "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
- )
- )
- self.queue_action("cancel", timeout=2000)
- else:
- self._cancel()
-
- def is_enqueue_action(self, force=False) -> bool:
- if force:
- return True
-
- if frappe.flags.in_test:
- return False
-
- # If line items are more than 100 or record is older than 6 months
- if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
- return True
-
- return False
-
def on_submit(self):
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 9d1a3f7..39166e2 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1642,36 +1642,6 @@
self.assertRaises(frappe.ValidationError, sr_doc.submit)
- def test_enqueue_action(self):
- frappe.flags.in_test = False
- item_code = "Test Enqueue Item - 001"
- create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
-
- doc = make_stock_entry(
- item_code=item_code,
- posting_date=add_to_date(today(), months=-7),
- posting_time="00:00:00",
- purpose="Material Receipt",
- qty=10,
- to_warehouse="_Test Warehouse - _TC",
- do_not_submit=True,
- )
-
- self.assertTrue(doc.is_enqueue_action())
-
- doc = make_stock_entry(
- item_code=item_code,
- posting_date=today(),
- posting_time="00:00:00",
- purpose="Material Receipt",
- qty=10,
- to_warehouse="_Test Warehouse - _TC",
- do_not_submit=True,
- )
-
- self.assertFalse(doc.is_enqueue_action())
- frappe.flags.in_test = True
-
def test_negative_batch(self):
item_code = "Test Negative Batch Item - 001"
make_item(
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 0450038..4738a70 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -1061,6 +1061,77 @@
self.assertTrue(frappe.db.get_value("Purchase Receipt", {"subcontracting_receipt": scr.name}))
+ def test_use_serial_batch_fields_for_subcontracting_receipt(self):
+ fg_item = make_item(
+ "Test Subcontracted Item With Batch No",
+ properties={
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BATCH-BNGS-.####",
+ "is_sub_contracted_item": 1,
+ },
+ ).name
+
+ make_item(
+ "Test Subcontracted Item With Batch No Service Item 1",
+ properties={"is_stock_item": 0},
+ )
+
+ make_bom(
+ item=fg_item,
+ raw_materials=[
+ make_item(
+ "Test Subcontracted Item With Batch No RM Item 1",
+ properties={
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BATCH-RM-BNGS-.####",
+ },
+ ).name
+ ],
+ )
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Test Subcontracted Item With Batch No Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 1,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ batch_no = "BATCH-BNGS-0001"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc(
+ {
+ "doctype": "Batch",
+ "batch_id": batch_no,
+ "item": fg_item,
+ }
+ ).insert()
+
+ scr = make_subcontracting_receipt(sco.name)
+ self.assertFalse(scr.items[0].serial_and_batch_bundle)
+ scr.items[0].use_serial_batch_fields = 1
+ scr.items[0].batch_no = batch_no
+
+ scr.save()
+ scr.submit()
+ scr.reload()
+ self.assertTrue(scr.items[0].serial_and_batch_bundle)
+
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)