From 4b15c00b11ee9ed2271ef55d72982de436106f34 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:59:21 +0530 Subject: [PATCH] fix: use serial batch fields for subcontracting receipt (backport #40311) (#40315) fix: use serial batch fields for subcontracting receipt (#40311) (cherry picked from commit cef62913118dc7400066d1f04c81025d96daf015) Co-authored-by: rohitwaghchaure --- erpnext/controllers/queries.py | 12 +++ erpnext/controllers/stock_controller.py | 80 ++++++++++--------- .../js/utils/serial_no_batch_selector.js | 9 ++- .../serial_and_batch_bundle.py | 7 ++ .../stock/doctype/stock_entry/stock_entry.py | 47 +---------- .../doctype/stock_entry/test_stock_entry.py | 30 ------- .../test_subcontracting_receipt.py | 71 ++++++++++++++++ 7 files changed, 143 insertions(+), 113 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 9904c75e98a..a597d5f20a5 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -434,9 +434,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): 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 874cbb16550..63556cddca4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -190,43 +190,23 @@ class StockController(AccountsController): 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 @@ class StockController(AccountsController): } ) + 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 fccaf88c719..d0064b30c34 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 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { 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 dd59a5dd33f..08cb3ca3074 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 @@ def get_type_of_transaction(parent_doc, child_row): 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 759fc080aca..5a1d22ecd6d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -10,17 +10,7 @@ import frappe 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 @@ class StockEntry(StockController): 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 699f5264c5e..902b8ffa90c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1633,36 +1633,6 @@ class TestStockEntry(FrappeTestCase): 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 0450038d803..4738a700408 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 @@ class TestSubcontractingReceipt(FrappeTestCase): 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)