diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 5a281aaa4fdb..ad2889d0a0af 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -80,13 +80,16 @@ "target_warehouse", "quality_inspection", "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "col_break5", "allow_zero_valuation_rate", - "serial_no", "item_tax_rate", "actual_batch_qty", "actual_qty", + "section_break_tlhi", + "serial_no", + "column_break_ciit", + "batch_no", "edit_references", "sales_order", "so_detail", @@ -628,13 +631,13 @@ "options": "Quality Inspection" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "hidden": 1, "label": "Batch No", "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "col_break5", @@ -649,14 +652,14 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "hidden": 1, "in_list_view": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "read_only": 1 + "oldfieldtype": "Small Text" }, { "fieldname": "item_tax_rate", @@ -824,17 +827,33 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_tlhi", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ciit", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-11-14 18:33:22.585715", + "modified": "2024-02-04 16:36:25.665743", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py index e2a62f1336e5..55a577b0c512 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py @@ -82,6 +82,7 @@ class POSInvoiceItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cb0b8e1fb11b..45b248264001 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -696,6 +696,7 @@ def on_submit(self): # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() if self.is_old_subcontracting_flow: diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 26984d96efd0..3ee4214ae71e 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -62,16 +62,19 @@ "rm_supp_cost", "warehouse_section", "warehouse", - "from_warehouse", - "quality_inspection", "add_serial_batch_bundle", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "col_br_wh", + "from_warehouse", + "quality_inspection", "rejected_warehouse", "rejected_serial_and_batch_bundle", - "batch_no", + "section_break_rqbe", + "serial_no", "rejected_serial_no", + "column_break_vbbb", + "batch_no", "manufacture_details", "manufacturer", "column_break_13", @@ -440,13 +443,11 @@ "print_hide": 1 }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -454,21 +455,18 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "serial_no", "fieldtype": "Text", - "hidden": 1, - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "rejected_serial_no", "fieldtype": "Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "accounting", @@ -891,7 +889,7 @@ "label": "Apply TDS" }, { - "depends_on": "eval:parent.update_stock == 1", + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -901,7 +899,7 @@ "search_index": 1 }, { - "depends_on": "eval:parent.update_stock == 1", + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -916,16 +914,31 @@ "options": "Asset" }, { - "depends_on": "eval:parent.update_stock === 1", + "depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", + "fieldname": "section_break_rqbe", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_vbbb", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-01-21 19:46:25.537861", + "modified": "2024-02-04 14:11:52.742228", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index e48d22379a6b..ccbc34749d75 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document): stock_uom_rate: DF.Currency total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link | None weight_per_unit: DF.Float diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 343f3033bfb9..bbfe6a38d8d6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -446,6 +446,7 @@ def on_submit(self): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() # this sequence because outstanding may get -ve diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index ec9e792d7d43..d06c7861da7d 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -83,14 +83,17 @@ "quality_inspection", "pick_serial_and_batch", "serial_and_batch_bundle", - "batch_no", - "incoming_rate", + "use_serial_batch_fields", "col_break5", "allow_zero_valuation_rate", - "serial_no", + "incoming_rate", "item_tax_rate", "actual_batch_qty", "actual_qty", + "section_break_eoec", + "serial_no", + "column_break_ytgd", + "batch_no", "edit_references", "sales_order", "so_detail", @@ -600,12 +603,11 @@ "options": "Quality Inspection" }, { + "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -621,13 +623,12 @@ "print_hide": 1 }, { + "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "serial_no", "fieldtype": "Small Text", - "hidden": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "read_only": 1 + "oldfieldtype": "Small Text" }, { "fieldname": "item_group", @@ -891,6 +892,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -904,12 +906,27 @@ "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1", + "fieldname": "section_break_eoec", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ytgd", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-12-29 13:03:14.121298", + "modified": "2024-02-04 11:52:16.106541", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 80f67748f40b..c71d08e7f70c 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -86,6 +86,7 @@ class SalesInvoiceItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index cad74df51e1b..66014904cc42 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -126,6 +126,7 @@ def before_submit(self): self.create_target_asset() def on_submit(self): + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.update_target_asset() diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index 26e1c3c270f9..8eda441781f2 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -18,9 +18,12 @@ "amount", "batch_and_serial_no_section", "serial_and_batch_bundle", + "use_serial_batch_fields", "column_break_13", - "batch_no", + "section_break_bfqc", "serial_no", + "column_break_mbuv", + "batch_no", "accounting_dimensions_section", "cost_center", "dimension_col_break" @@ -39,13 +42,13 @@ "reqd": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "section_break_6", @@ -102,12 +105,12 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "hidden": 1, "label": "Serial No", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "item_code", @@ -148,18 +151,34 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_bfqc", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_mbuv", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-06 01:10:17.947952", + "modified": "2024-02-04 16:41:09.239762", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Stock Item", diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py index 122cbb600d64..d2b075c3e680 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document): serial_no: DF.SmallText | None stock_qty: DF.Float stock_uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index c8516820ef38..ba3cdc8e833c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -21,6 +21,9 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( get_evaluated_inventory_dimension, ) +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_type_of_transaction, +) from erpnext.stock.stock_ledger import get_items_to_be_repost @@ -126,6 +129,81 @@ def clean_serial_nos(self): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) + def make_bundle_using_old_serial_batch_fields(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + # To handle test cases + if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields: + return + + table_name = "items" + if self.doctype == "Asset Capitalization": + table_name = "stock_items" + + for row in self.get(table_name): + if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): + continue + + if not row.use_serial_batch_fields and ( + row.serial_no or row.batch_no or row.get("rejected_serial_no") + ): + frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + + 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" + else: + qty = row.stock_qty + type_of_transaction = get_type_of_transaction(self, row) + + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.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() + + if sn_doc.is_rejected: + row.rejected_serial_and_batch_bundle = sn_doc.name + row.db_set( + { + "rejected_serial_and_batch_bundle": sn_doc.name, + "rejected_serial_no": "", + } + ) + else: + row.serial_and_batch_bundle = sn_doc.name + row.db_set( + { + "serial_and_batch_bundle": sn_doc.name, + "serial_no": "", + "batch_no": "", + } + ) + + def set_use_serial_batch_fields(self): + if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): + for row in self.items: + row.use_serial_batch_fields = 1 + def get_gl_entries( self, warehouse_account=None, default_expense_account=None, default_cost_center=None ): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5da6d7ec610a..0241afcf030e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe super.setup(); let me = this; + this.set_fields_onload_for_line_item(); this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; frappe.flags.hide_serial_batch_dialog = true; @@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { + debugger var item = frappe.get_doc(cdt, cdn); if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; @@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.from_warehouse = frm.doc.set_from_warehouse; } + if (item.docstatus === 0 + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); @@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }; }); } + } + set_fields_onload_for_line_item() { + if (this.frm.is_new && this.frm.doc?.items) { + this.frm.doc.items.forEach(item => { + if (item.docstatus === 0 + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + }) + } } toggle_enable_for_stock_uom(field) { @@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.doc.doctype === 'Delivery Note') { show_batch_dialog = 1; } + + if (show_batch_dialog && item.use_serial_batch_fields === 1) { + show_batch_dialog = 0; + } + item.barcode = null; @@ -706,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.serial_no = item.serial_no.replace(/,/g, '\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); - if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { + if (!doc.is_return) { setTimeout(() => { me.update_qty(cdt, cdn); - }, 10000); + }, 3000); } } } @@ -1242,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } - sync_bundle_data() { - let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"]; - - if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.sync_bundle_data(); - barcode_scanner.remove_item_from_localstorage(); - } - } - - before_save(doc) { - this.sync_bundle_data(); - } - service_start_date(frm, cdt, cdn) { var child = locals[cdt][cdn]; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index aacab0fe6c19..4d1c0c1ad3de 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -1,12 +1,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { constructor(opts) { this.frm = opts.frm; + // frappe.flags.trigger_from_barcode_scanner is used for custom scripts // field from which to capture input of scanned data this.scan_field_name = opts.scan_field_name || "scan_barcode"; this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name]; this.barcode_field = opts.barcode_field || "barcode"; + this.serial_no_field = opts.serial_no_field || "serial_no"; + this.batch_no_field = opts.batch_no_field || "batch_no"; this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; // field name on row which defines max quantity to be scanned e.g. picklist @@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.frm.has_items = false; } - if (serial_no) { - this.is_duplicate_serial_no(row, item_code, serial_no) - .then((is_duplicate) => { - if (!is_duplicate) { - this.run_serially_tasks(row, data, resolve); - } else { - this.clean_up(); - reject(); - return; - } - }); - } else { - this.run_serially_tasks(row, data, resolve); + if (this.is_duplicate_serial_no(row, serial_no)) { + this.clean_up(); + reject(); + return; } - + frappe.run_serially([ + () => this.set_selector_trigger_flag(data), + () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { + this.show_scan_message(row.idx, row.item_code, qty); + }), + () => this.set_barcode_uom(row, uom), + () => this.set_serial_no(row, serial_no), + () => this.set_batch_no(row, batch_no), + () => this.set_barcode(row, barcode), + () => this.clean_up(), + () => this.revert_selector_flag(), + () => resolve(row) + ]); }); } - run_serially_tasks(row, data, resolve) { - const {item_code, barcode, batch_no, serial_no, uom} = data; - - frappe.run_serially([ - () => this.set_serial_and_batch(row, item_code, serial_no, batch_no), - () => this.set_barcode(row, barcode), - () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { - this.show_scan_message(row.idx, row.item_code, qty); - }), - () => this.set_barcode_uom(row, uom), - () => this.clean_up(), - () => { - if (row.serial_and_batch_bundle && !this.frm.is_new()) { - this.frm.save(); - } + // batch and serial selector is reduandant when all info can be added by scan + // this flag on item row is used by transaction.js to avoid triggering selector + set_selector_trigger_flag(data) { + const {batch_no, serial_no, has_batch_no, has_serial_no} = data; - frappe.flags.trigger_from_barcode_scanner = false; - }, - () => resolve(row), - ]); + const require_selecting_batch = has_batch_no && !batch_no; + const require_selecting_serial = has_serial_no && !serial_no; + + if (!(require_selecting_batch || require_selecting_serial)) { + frappe.flags.hide_serial_batch_dialog = true; + } + } + + revert_selector_flag() { + frappe.flags.hide_serial_batch_dialog = false; + frappe.flags.trigger_from_barcode_scanner = false; } set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { - const item_data = {item_code: item_code}; - item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); + const item_data = {item_code: item_code, use_serial_batch_fields: 1.0}; frappe.flags.trigger_from_barcode_scanner = true; + item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); await frappe.model.set_value(row.doctype, row.name, item_data); return value; }; @@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { increment(value).then((value) => resolve(value)); }); + } else if (this.frm.has_items) { + this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no); } else { increment().then((value) => resolve(value)); } @@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.model.set_value(row.doctype, row.name, item_data); frappe.run_serially([ + () => this.set_batch_no(row, this.dialog.get_value("batch_no")), () => this.set_barcode(row, this.dialog.get_value("barcode")), - () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")), + () => this.set_serial_no(row, this.dialog.get_value("serial_no")), () => this.add_child_for_remaining_qty(row), () => this.clean_up() ]); @@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - async set_serial_and_batch(row, item_code, serial_no, batch_no) { - if (this.frm.is_new() || !row.serial_and_batch_bundle) { - this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no); - } else if(row.serial_and_batch_bundle) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch", - args: { - bundle_id: row.serial_and_batch_bundle, - serial_no: serial_no, - batch_no: batch_no, - }, - }) - } - } - - get_key_for_localstorage() { - let parts = this.frm.doc.name.split("-"); - return parts[parts.length - 1] + this.frm.doc.doctype; - } - - update_localstorage_scanned_data() { - let docname = this.frm.doc.name - if (localStorage[docname]) { - let items = JSON.parse(localStorage[docname]); - let existing_items = this.frm.doc.items.map(d => d.item_code); - if (!existing_items.length) { - localStorage.removeItem(docname); - return; - } - - for (let item_code in items) { - if (!existing_items.includes(item_code)) { - delete items[item_code]; - } - } - - localStorage[docname] = JSON.stringify(items); - } - } - - async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) { - let docname = this.frm.doc.name - - let entries = JSON.parse(localStorage.getItem(docname)); - if (!entries) { - entries = {}; - } - - let key = item_code; - if (!entries[key]) { - entries[key] = []; - } - - let existing_row = []; - if (!serial_no && batch_no) { - existing_row = entries[key].filter((e) => e.batch_no === batch_no); - if (existing_row.length) { - existing_row[0].qty += 1; - } - } else if (serial_no) { - existing_row = entries[key].filter((e) => e.serial_no === serial_no); - if (existing_row.length) { - frappe.throw(__("Serial No {0} has already scanned.", [serial_no])); - } - } - - if (!existing_row.length) { - entries[key].push({ - "serial_no": serial_no, - "batch_no": batch_no, - "qty": 1 - }); - } - - localStorage.setItem(docname, JSON.stringify(entries)); - - // Auto remove from localstorage after 1 hour - setTimeout(() => { - localStorage.removeItem(docname); - }, 3600000) - } - - remove_item_from_localstorage() { - let docname = this.frm.doc.name; - if (localStorage[docname]) { - localStorage.removeItem(docname); - } - } + async set_serial_no(row, serial_no) { + if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) { + const existing_serial_nos = row[this.serial_no_field]; + let new_serial_nos = ""; - async sync_bundle_data() { - let docname = this.frm.doc.name; - - if (localStorage[docname]) { - let entries = JSON.parse(localStorage[docname]); - if (entries) { - for (let entry in entries) { - let row = this.frm.doc.items.filter((item) => { - if (item.item_code === entry) { - return true; - } - })[0]; - - if (row) { - this.create_serial_and_batch_bundle(row, entries, entry) - .then(() => { - if (!entries) { - localStorage.removeItem(docname); - } - }); - } - } + if (!!existing_serial_nos) { + new_serial_nos = existing_serial_nos + "\n" + serial_no; + } else { + new_serial_nos = serial_no; } + await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos); } } - async create_serial_and_batch_bundle(row, entries, key) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers", - args: { - entries: entries[key], - child_row: row, - doc: this.frm.doc, - warehouse: row.warehouse, - do_not_save: 1 - }, - callback: function(r) { - row.serial_and_batch_bundle = r.message.name; - delete entries[key]; - } - }) - } - async set_barcode_uom(row, uom) { if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) { await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom); } } + async set_batch_no(row, batch_no) { + if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) { + await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no); + } + } + async set_barcode(row, barcode) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); @@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - async is_duplicate_serial_no(row, item_code, serial_no) { - let is_duplicate = false; - const promise = new Promise((resolve, reject) => { - if (this.frm.is_new() || !row.serial_and_batch_bundle) { - is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no); - if (is_duplicate) { - this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); - } - - resolve(is_duplicate); - } else if (row.serial_and_batch_bundle) { - this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { - if (r.message) { - this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); - } - - is_duplicate = r.message; - resolve(is_duplicate); - }) - } - }); - - return await promise; - } + is_duplicate_serial_no(row, serial_no) { + const is_duplicate = row[this.serial_no_field]?.includes(serial_no); - check_duplicate_serial_no_in_db(row, serial_no, response) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no", - args: { - serial_no: serial_no, - bundle_id: row.serial_and_batch_bundle - }, - callback(r) { - response(r); - } - }); - } - - check_duplicate_serial_no_in_localstorage(item_code, serial_no) { - let docname = this.frm.doc.name - let entries = JSON.parse(localStorage.getItem(docname)); - - if (!entries) { - return false; - } - - let existing_row = []; - if (entries[item_code]) { - existing_row = entries[item_code].filter((e) => e.serial_no === serial_no); + if (is_duplicate) { + this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); } - - return existing_row.length; + return is_duplicate; } get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { @@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { show_alert(msg, indicator, duration=3) { frappe.show_alert({message: msg, indicator: indicator}, duration); } -}; +}; \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5ef2c50146ae..f00e6ac5122f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -904,6 +904,7 @@ def set_missing_values(source, target): target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") + target.run_method("set_use_serial_batch_fields") if source.company_address: target.update({"company_address": source.company_address}) @@ -1024,6 +1025,7 @@ def set_missing_values(source, target): target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") + target.run_method("set_use_serial_batch_fields") if source.company_address: target.update({"company_address": source.company_address}) @@ -1606,7 +1608,11 @@ def should_pick_order_item(item) -> bool: "Sales Order", source_name, { - "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order": { + "doctype": "Pick List", + "field_map": {"set_warehouse": "parent_warehouse"}, + "validation": {"docstatus": ["=", 1]}, + }, "Sales Order Item": { "doctype": "Pick List Item", "field_map": {"parent": "sales_order", "name": "sales_order_item"}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7d7b0cd4769f..df45fdded894 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -398,6 +398,8 @@ def on_submit(self): self.check_credit_limit() elif self.issue_credit_note: self.make_return_invoice() + + self.make_bundle_using_old_serial_batch_fields() # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index dae42895edbf..7889f95c6053 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -189,7 +189,6 @@ def test_delivery_note_return_against_denormalized_serial_no(self): }, ) - frappe.flags.ignore_serial_batch_bundle_validation = True serial_nos = [ "OSN-1", "OSN-2", @@ -228,6 +227,8 @@ def test_delivery_note_return_against_denormalized_serial_no(self): ) se_doc.items[0].serial_no = "\n".join(serial_nos) + + frappe.flags.use_serial_and_batch_fields = True se_doc.submit() self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos)) @@ -283,6 +284,8 @@ def test_delivery_note_return_against_denormalized_serial_no(self): self.assertTrue(serial_no in serial_nos) self.assertFalse(serial_no in returned_serial_nos1) + frappe.flags.use_serial_and_batch_fields = False + def test_sales_return_for_non_bundled_items_partial(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -1552,7 +1555,7 @@ def create_delivery_note(**args): dn.return_against = args.return_against bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")): type_of_transaction = args.type_of_transaction or "Outward" if dn.is_return: @@ -1594,6 +1597,9 @@ def create_delivery_note(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "target_warehouse": args.target_warehouse, + "use_serial_batch_fields": args.use_serial_batch_fields, + "serial_no": args.serial_no if args.use_serial_batch_fields else None, + "batch_no": args.batch_no if args.use_serial_batch_fields else None, }, ) 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 a44b9ac44bec..247672fe1260 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -80,8 +80,11 @@ "section_break_40", "pick_serial_and_batch", "serial_and_batch_bundle", + "use_serial_batch_fields", "column_break_eaoe", + "section_break_qyjv", "serial_no", + "column_break_rxvc", "batch_no", "available_qty_section", "actual_batch_qty", @@ -850,6 +853,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -859,6 +863,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" @@ -874,27 +879,40 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", - "hidden": 1, - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_qyjv", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_rxvc", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:38.638144", + "modified": "2024-02-04 14:10:31.750340", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index c11c4103e59b..b76f74297283 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -82,6 +82,7 @@ class DeliveryNoteItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 5dd8934d43f1..1daf6791d400 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -20,9 +20,12 @@ "uom", "section_break_9", "pick_serial_and_batch", + "use_serial_batch_fields", + "column_break_11", "serial_and_batch_bundle", + "section_break_bgys", "serial_no", - "column_break_11", + "column_break_qlha", "batch_no", "actual_batch_qty", "section_break_13", @@ -118,10 +121,10 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "column_break_11", @@ -131,8 +134,7 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { "fieldname": "section_break_13", @@ -259,6 +261,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -267,16 +270,32 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_bgys", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_qlha", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-28 13:16:38.460806", + "modified": "2024-02-04 16:30:44.263964", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index ed667c2b992f..c115e33e1716 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -47,6 +47,7 @@ class PackedItem(Document): serial_no: DF.Text | None target_warehouse: DF.Link | None uom: DF.Link | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index afd6ce81386c..aa0e12549681 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', { frm.set_query('parent_warehouse', () => { return { filters: { - 'is_group': 1, 'company': frm.doc.company } }; diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 7259dc00a81b..bd84aadef74b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -51,7 +51,7 @@ "description": "Items under this warehouse will be suggested", "fieldname": "parent_warehouse", "fieldtype": "Link", - "label": "Parent Warehouse", + "label": "Warehouse", "options": "Warehouse" }, { @@ -188,7 +188,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-01-24 10:33:43.244476", + "modified": "2024-02-01 16:17:44.877426", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 758448af797f..e2edb20510cf 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -13,7 +13,7 @@ from frappe.query_builder import Case from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum -from frappe.utils import cint, floor, flt +from frappe.utils import ceil, cint, floor, flt from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( @@ -122,11 +122,42 @@ def validate_picked_items(self): def on_submit(self): self.validate_serial_and_batch_bundle() + self.make_bundle_using_old_serial_batch_fields() self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() + def make_bundle_using_old_serial_batch_fields(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in self.locations: + if not row.serial_no and not row.batch_no: + continue + + if not row.use_serial_batch_fields and (row.serial_no or row.batch_no): + frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + + if row.use_serial_batch_fields and (not row.serial_and_batch_bundle): + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "qty": row.stock_qty, + "type_of_transaction": "Outward", + "company": self.company, + "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, + "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None, + "batch_no": row.batch_no, + } + ).make_serial_and_batch_bundle() + + row.serial_and_batch_bundle = sn_doc.name + row.db_set("serial_and_batch_bundle", sn_doc.name) + def on_update_after_submit(self) -> None: if self.has_reserved_stock(): msg = _( @@ -156,6 +187,7 @@ def delink_serial_and_batch_bundle(self): {"is_cancelled": 1, "voucher_no": ""}, ) + frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel() row.db_set("serial_and_batch_bundle", None) def on_update(self): @@ -324,7 +356,6 @@ def set_item_locations(self, save=False): locations_replica = self.get("locations") # reset - self.remove_serial_and_batch_bundle() self.delete_key("locations") updated_locations = frappe._dict() for item_doc in items: @@ -639,13 +670,19 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) if not stock_qty: break + serial_nos = None + if item_location.serial_nos: + serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)]) + locations.append( frappe._dict( { "qty": qty, "stock_qty": stock_qty, "warehouse": item_location.warehouse, - "serial_and_batch_bundle": item_location.serial_and_batch_bundle, + "serial_no": serial_nos, + "batch_no": item_location.batch_no, + "use_serial_batch_fields": 1, } ) ) @@ -681,7 +718,15 @@ def get_available_item_locations( has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") - if has_serial_no: + if has_batch_no and has_serial_no: + locations = get_available_item_locations_for_serial_and_batched_item( + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + ) + elif has_serial_no: locations = get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company, total_picked_qty ) @@ -724,6 +769,47 @@ def get_available_item_locations( return locations +def get_available_item_locations_for_serial_and_batched_item( + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, +): + # Get batch nos by FIFO + locations = get_available_item_locations_for_batched_item( + item_code, + from_warehouses, + required_qty, + company, + ) + + if locations: + sn = frappe.qb.DocType("Serial No") + conditions = (sn.item_code == item_code) & (sn.company == company) + + for location in locations: + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch + + serial_nos = ( + frappe.qb.from_(sn) + .select(sn.name) + .where( + (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) + ) + .orderby(sn.creation) + .limit(ceil(location.qty + total_picked_qty)) + ).run(as_dict=True) + + serial_nos = [sn.name for sn in serial_nos] + location.serial_nos = serial_nos + location.qty = len(serial_nos) + + return locations + + def get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): @@ -757,28 +843,16 @@ def get_available_item_locations_for_serialized_item( picked_qty -= 1 locations = [] + for warehouse, serial_nos in warehouse_serial_nos_map.items(): qty = len(serial_nos) - bundle_doc = SerialBatchCreation( - { - "item_code": item_code, - "warehouse": warehouse, - "voucher_type": "Pick List", - "total_qty": qty * -1, - "serial_nos": serial_nos, - "type_of_transaction": "Outward", - "company": company, - "do_not_submit": True, - } - ).make_serial_and_batch_bundle() - locations.append( { "qty": qty, "warehouse": warehouse, "item_code": item_code, - "serial_and_batch_bundle": bundle_doc.name, + "serial_nos": serial_nos, } ) @@ -808,29 +882,17 @@ def get_available_item_locations_for_batched_item( warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty for warehouse, batches in warehouse_wise_batches.items(): - qty = sum(batches.values()) - - bundle_doc = SerialBatchCreation( - { - "item_code": item_code, - "warehouse": warehouse, - "voucher_type": "Pick List", - "total_qty": qty * -1, - "batches": batches, - "type_of_transaction": "Outward", - "company": company, - "do_not_submit": True, - } - ).make_serial_and_batch_bundle() - - locations.append( - { - "qty": qty, - "warehouse": warehouse, - "item_code": item_code, - "serial_and_batch_bundle": bundle_doc.name, - } - ) + for batch_no, qty in batches.items(): + locations.append( + frappe._dict( + { + "qty": qty, + "warehouse": warehouse, + "item_code": item_code, + "batch_no": batch_no, + } + ) + ) return locations diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 322b0b46baac..cffd0d2820ff 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -217,6 +217,8 @@ def test_pick_list_shows_serial_no_for_serialized_item(self): ) pick_list.save() + pick_list.submit() + self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) @@ -239,7 +241,7 @@ def test_pick_list_shows_batch_no_for_batched_item(self): pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0) pr1.load_from_db() - oldest_batch_no = pr1.items[0].batch_no + oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0) @@ -302,6 +304,8 @@ def test_pick_list_for_batched_and_serialised_item(self): } ) pick_list.set_item_locations() + pick_list.submit() + pick_list.reload() self.assertEqual( get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no @@ -310,6 +314,7 @@ def test_pick_list_for_batched_and_serialised_item(self): get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos ) + pick_list.cancel() pr1.cancel() pr2.cancel() @@ -671,29 +676,22 @@ def test_picklist_for_batch_item(self): so = make_sales_order(item_code=item, qty=25.0, rate=100) pl = create_pick_list(so.name) + pl.submit() # pick half the qty for loc in pl.locations: self.assertEqual(loc.qty, 25.0) self.assertTrue(loc.serial_and_batch_bundle) - data = frappe.get_all( - "Serial and Batch Entry", - fields=["qty", "batch_no"], - filters={"parent": loc.serial_and_batch_bundle}, - ) - - for d in data: - self.assertEqual(d.batch_no, "PICKLT-000001") - self.assertEqual(d.qty, 25.0 * -1) - pl.save() pl.submit() so1 = make_sales_order(item_code=item, qty=10.0, rate=100) - pl = create_pick_list(so1.name) + pl1 = create_pick_list(so1.name) + pl1.submit() + # pick half the qty - for loc in pl.locations: - self.assertEqual(loc.qty, 10.0) + for loc in pl1.locations: + self.assertEqual(loc.qty, 5.0) self.assertTrue(loc.serial_and_batch_bundle) data = frappe.get_all( @@ -709,8 +707,7 @@ def test_picklist_for_batch_item(self): elif d.batch_no == "PICKLT-000002": self.assertEqual(d.qty, 5.0 * -1) - pl.save() - pl.submit() + pl1.cancel() pl.cancel() def test_picklist_for_serial_item(self): @@ -723,6 +720,7 @@ def test_picklist_for_serial_item(self): so = make_sales_order(item_code=item, qty=25.0, rate=100) pl = create_pick_list(so.name) + pl.submit() picked_serial_nos = [] # pick half the qty for loc in pl.locations: @@ -736,13 +734,11 @@ def test_picklist_for_serial_item(self): picked_serial_nos = [d.serial_no for d in data] self.assertEqual(len(picked_serial_nos), 25) - pl.save() - pl.submit() - so1 = make_sales_order(item_code=item, qty=10.0, rate=100) - pl = create_pick_list(so1.name) + pl1 = create_pick_list(so1.name) + pl1.submit() # pick half the qty - for loc in pl.locations: + for loc in pl1.locations: self.assertEqual(loc.qty, 10.0) self.assertTrue(loc.serial_and_batch_bundle) @@ -756,8 +752,7 @@ def test_picklist_for_serial_item(self): for d in data: self.assertTrue(d.serial_no not in picked_serial_nos) - pl.save() - pl.submit() + pl1.cancel() pl.cancel() def test_picklist_with_bundles(self): diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index e8e4afc6e3f3..962fa9f09de1 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -24,8 +24,11 @@ "serial_no_and_batch_section", "pick_serial_and_batch", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "column_break_20", + "section_break_ecxc", + "serial_no", + "column_break_belw", "batch_no", "column_break_15", "sales_order", @@ -72,19 +75,17 @@ "read_only": 1 }, { - "depends_on": "serial_no", + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { - "depends_on": "batch_no", + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -195,6 +196,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -204,6 +206,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" @@ -218,11 +221,26 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_ecxc", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_belw", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-07-26 12:54:15.785962", + "modified": "2024-02-04 16:12:16.257951", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index 6e5a94e44658..f3f6298a305c 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -37,6 +37,7 @@ class PickListItem(Document): stock_reserved_qty: DF.Float stock_uom: DF.Link | None uom: DF.Link | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8da059663176..28d55f6ce3a7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -368,6 +368,7 @@ def on_submit(self): else: self.db_set("status", "Completed") + self.make_bundle_using_old_serial_batch_fields() # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty, reserved_qty_for_subcontract in bin # depends upon updated ordered qty in PO diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index dd49eabeaf87..ff0300f9e969 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2193,6 +2193,93 @@ def non_internal_transfer_purchase_receipt(self): pr_doc.reload() self.assertFalse(pr_doc.items[0].from_warehouse) + def test_use_serial_batch_fields_for_serial_nos(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + item_code = make_item( + "_Test Use Serial Fields Item Serial Item", + properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"}, + ).name + + serial_nos = [ + "SNU-TSFISI-000011", + "SNU-TSFISI-000012", + "SNU-TSFISI-000013", + "SNU-TSFISI-000014", + "SNU-TSFISI-000015", + ] + + pr = make_purchase_receipt( + item_code=item_code, + qty=5, + serial_no="\n".join(serial_nos), + use_serial_batch_fields=1, + rate=100, + ) + + self.assertEqual(pr.items[0].use_serial_batch_fields, 1) + self.assertFalse(pr.items[0].serial_no) + self.assertTrue(pr.items[0].serial_and_batch_bundle) + + sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle) + + for row in sbb_doc.entries: + self.assertTrue(row.serial_no in serial_nos) + + serial_nos.remove("SNU-TSFISI-000015") + + sr = create_stock_reconciliation( + item_code=item_code, + serial_no="\n".join(serial_nos), + qty=4, + warehouse=pr.items[0].warehouse, + use_serial_batch_fields=1, + do_not_submit=True, + ) + sr.reload() + + serial_nos = get_serial_nos(sr.items[0].current_serial_no) + self.assertEqual(len(serial_nos), 5) + self.assertEqual(sr.items[0].current_qty, 5) + + new_serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(new_serial_nos), 4) + self.assertEqual(sr.items[0].qty, 4) + self.assertEqual(sr.items[0].use_serial_batch_fields, 1) + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + self.assertFalse(sr.items[0].serial_and_batch_bundle) + self.assertTrue(sr.items[0].current_serial_no) + sr.submit() + + sr.reload() + self.assertTrue(sr.items[0].current_serial_and_batch_bundle) + self.assertTrue(sr.items[0].serial_and_batch_bundle) + + serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status") + + self.assertTrue(serial_no_status != "Active") + + dn = create_delivery_note( + item_code=item_code, + qty=4, + serial_no="\n".join(new_serial_nos), + use_serial_batch_fields=1, + ) + + self.assertTrue(dn.items[0].serial_and_batch_bundle) + self.assertEqual(dn.items[0].qty, 4) + doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) + for row in doc.entries: + self.assertTrue(row.serial_no in new_serial_nos) + + for sn in new_serial_nos: + serial_no_status = frappe.db.get_value("Serial No", sn, "status") + self.assertTrue(serial_no_status != "Active") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -2361,7 +2448,7 @@ def make_purchase_receipt(**args): uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")): batches = {} if args.get("batch_no"): batches = frappe._dict({args.batch_no: qty}) @@ -2403,6 +2490,9 @@ def make_purchase_receipt(**args): "cost_center": args.cost_center or frappe.get_cached_value("Company", pr.company, "cost_center"), "asset_location": args.location or "Test Location", + "use_serial_batch_fields": args.use_serial_batch_fields or 0, + "serial_no": args.serial_no if args.use_serial_batch_fields else "", + "batch_no": args.batch_no if args.use_serial_batch_fields else "", }, ) 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 9bd692ad618f..6b01047f0062 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -94,6 +94,7 @@ "section_break_45", "add_serial_batch_bundle", "serial_and_batch_bundle", + "use_serial_batch_fields", "col_break5", "add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle", @@ -1003,6 +1004,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -1020,24 +1022,22 @@ { "fieldname": "serial_no", "fieldtype": "Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "rejected_serial_no", "fieldtype": "Text", - "label": "Rejected Serial No", - "read_only": 1 + "label": "Rejected Serial No" }, { "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -1045,11 +1045,13 @@ "options": "Serial and Batch Bundle" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_for_rejected_qty", "fieldtype": "Button", "label": "Add Serial / Batch No (Rejected Qty)" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "section_break_3vxt", "fieldtype": "Section Break" }, @@ -1058,6 +1060,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" @@ -1098,12 +1101,18 @@ "read_only": 1, "report_hide": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-12-25 22:32:09.801965", + "modified": "2024-02-04 11:48:06.653771", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py index aed8d21dae7e..3c6dcdca4881 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document): supplier_part_no: DF.Data | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link | None weight_per_unit: DF.Float 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 9cad8f62b884..eb4df29db829 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 @@ -1117,7 +1117,7 @@ def parse_serial_nos(data): if isinstance(data, list): return data - return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()] + return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()] @frappe.whitelist() @@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers( def get_type_of_transaction(parent_doc, child_row): - type_of_transaction = child_row.type_of_transaction + type_of_transaction = child_row.get("type_of_transaction") if parent_doc.get("doctype") == "Stock Entry": type_of_transaction = "Outward" if child_row.s_warehouse else "Inward" @@ -1384,6 +1384,8 @@ def get_available_serial_nos(kwargs): filters = {"item_code": kwargs.item_code} + # ignore_warehouse is used for backdated stock transactions + # There might be chances that the serial no not exists in the warehouse during backdated stock transactions if not kwargs.get("ignore_warehouse"): filters["warehouse"] = ("is", "set") if kwargs.warehouse: @@ -1677,7 +1679,10 @@ def get_reserved_batches_for_sre(kwargs) -> dict: query = query.where(sb_entry.batch_no == kwargs.batch_no) if kwargs.warehouse: - query = query.where(sre.warehouse == kwargs.warehouse) + if isinstance(kwargs.warehouse, list): + query = query.where(sre.warehouse.isin(kwargs.warehouse)) + else: + query = query.where(sre.warehouse == kwargs.warehouse) if kwargs.ignore_voucher_nos: query = query.where(sre.name.notin(kwargs.ignore_voucher_nos)) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 0d453fb8418f..f43094370867 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -136,6 +136,7 @@ def test_inward_outward_batch_valuation(self): 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, @@ -240,6 +241,7 @@ def test_old_batch_valuation(self): 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 @@ -259,6 +261,7 @@ def test_old_serial_no_valuation(self): ) 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): @@ -320,6 +323,9 @@ def test_old_serial_no_valuation(self): 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 diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 122664c2dde9..5f4f3931a741 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -151,9 +151,7 @@ def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [ - s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() - ] + return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()] def clean_serial_no_string(serial_no: str) -> str: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 00cc8be4bb85..4239191383d1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -274,6 +274,7 @@ def is_enqueue_action(self, force=False) -> bool: def on_submit(self): self.validate_closed_subcontracting_order() + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.update_work_order() self.validate_subcontract_order() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 83bfaa0094c2..0f67e47ad9a1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -92,6 +92,9 @@ def process_serial_numbers(serial_nos_list): else: args.qty = cint(args.qty) + if args.serial_no or args.batch_no: + args.use_serial_batch_fields = True + # purpose if not args.purpose: if args.source and args.target: @@ -162,6 +165,7 @@ def process_serial_numbers(serial_nos_list): ) args.serial_no = serial_number + s.append( "items", { @@ -177,6 +181,7 @@ def process_serial_numbers(serial_nos_list): "batch_no": args.batch_no, "cost_center": args.cost_center, "expense_account": args.expense_account, + "use_serial_batch_fields": args.use_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 4e3214ebecaf..7ef2a0d5a0d4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -671,6 +671,7 @@ def test_serial_by_series(self): def test_serial_move(self): se = make_serialized_item() serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] + frappe.flags.use_serial_and_batch_fields = True se = frappe.copy_doc(test_records[0]) se.purpose = "Material Transfer" @@ -691,6 +692,7 @@ def test_serial_move(self): self.assertTrue( frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" ) + frappe.flags.use_serial_and_batch_fields = False def test_serial_cancel(self): se, serial_nos = self.test_serial_by_series() @@ -990,6 +992,8 @@ def test_same_serial_nos_in_repack_or_manufacture_entries(self): do_not_save=True, ) + frappe.flags.use_serial_and_batch_fields = True + cls_obj = SerialBatchCreation( { "type_of_transaction": "Inward", @@ -1026,84 +1030,7 @@ def test_same_serial_nos_in_repack_or_manufacture_entries(self): s2.submit() s2.cancel() - - # def test_retain_sample(self): - # from erpnext.stock.doctype.batch.batch import get_batch_qty - # from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - # create_warehouse("Test Warehouse for Sample Retention") - # frappe.db.set_value( - # "Stock Settings", - # None, - # "sample_retention_warehouse", - # "Test Warehouse for Sample Retention - _TC", - # ) - - # test_item_code = "Retain Sample Item" - # if not frappe.db.exists("Item", test_item_code): - # item = frappe.new_doc("Item") - # item.item_code = test_item_code - # item.item_name = "Retain Sample Item" - # item.description = "Retain Sample Item" - # item.item_group = "All Item Groups" - # item.is_stock_item = 1 - # item.has_batch_no = 1 - # item.create_new_batch = 1 - # item.retain_sample = 1 - # item.sample_quantity = 4 - # item.save() - - # receipt_entry = frappe.new_doc("Stock Entry") - # receipt_entry.company = "_Test Company" - # receipt_entry.purpose = "Material Receipt" - # receipt_entry.append( - # "items", - # { - # "item_code": test_item_code, - # "t_warehouse": "_Test Warehouse - _TC", - # "qty": 40, - # "basic_rate": 12, - # "cost_center": "_Test Cost Center - _TC", - # "sample_quantity": 4, - # }, - # ) - # receipt_entry.set_stock_entry_type() - # receipt_entry.insert() - # receipt_entry.submit() - - # retention_data = move_sample_to_retention_warehouse( - # receipt_entry.company, receipt_entry.get("items") - # ) - # retention_entry = frappe.new_doc("Stock Entry") - # retention_entry.company = retention_data.company - # retention_entry.purpose = retention_data.purpose - # retention_entry.append( - # "items", - # { - # "item_code": test_item_code, - # "t_warehouse": "Test Warehouse for Sample Retention - _TC", - # "s_warehouse": "_Test Warehouse - _TC", - # "qty": 4, - # "basic_rate": 12, - # "cost_center": "_Test Cost Center - _TC", - # "batch_no": get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), - # }, - # ) - # retention_entry.set_stock_entry_type() - # retention_entry.insert() - # retention_entry.submit() - - # qty_in_usable_warehouse = get_batch_qty( - # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), "_Test Warehouse - _TC", "_Test Item" - # ) - # qty_in_retention_warehouse = get_batch_qty( - # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), - # "Test Warehouse for Sample Retention - _TC", - # "_Test Item", - # ) - - # self.assertEqual(qty_in_usable_warehouse, 36) - # self.assertEqual(qty_in_retention_warehouse, 4) + frappe.flags.use_serial_and_batch_fields = False def test_quality_check(self): item_code = "_Test Item For QC" diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index bd84a2b0d993..c7b3daab82ab 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -47,9 +47,12 @@ "amount", "serial_no_batch", "add_serial_batch_bundle", - "serial_and_batch_bundle", + "use_serial_batch_fields", "col_break4", + "serial_and_batch_bundle", + "section_break_rdtg", "serial_no", + "column_break_prps", "batch_no", "accounting", "expense_account", @@ -289,27 +292,27 @@ "no_copy": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "label": "Serial No", "no_copy": 1, "oldfieldname": "serial_no", - "oldfieldtype": "Text", - "read_only": 1 + "oldfieldtype": "Text" }, { "fieldname": "col_break4", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "oldfieldname": "batch_no", "oldfieldtype": "Link", - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { "depends_on": "eval:parent.inspection_required && doc.t_warehouse", @@ -573,24 +576,41 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_rdtg", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_prps", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-01-12 11:56:04.626103", + "modified": "2024-02-04 16:16:47.606270", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index a6dd0faadfc1..47c443c51948 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -63,6 +63,7 @@ class StockEntryDetail(Document): transfer_qty: DF.Float transferred_qty: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency # end: auto-generated types diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index d8a3f2e33c15..c0999532d033 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -482,6 +482,8 @@ def test_batchwise_item_valuation_moving_average(self): (item, warehouses[0], batches[1], 1, 200), (item, warehouses[0], batches[0], 1, 200), ] + + frappe.flags.use_serial_and_batch_fields = True dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"]) svd_list = [-1 * d["stock_value_difference"] for d in sle_details] @@ -494,6 +496,8 @@ def test_batchwise_item_valuation_moving_average(self): "Incorrect 'Incoming Rate' values fetched for DN items", ) + frappe.flags.use_serial_and_batch_fields = False + def test_batchwise_item_valuation_stock_reco(self): item, warehouses, batches = setup_item_valuation_test() state = {"stock_value": 0.0, "qty": 0.0} diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 8e9dcb0fc522..ba7f9c58a8b1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -198,6 +198,7 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); + frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields); if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) { frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 788ae0d3abc1..ce08615ed5c0 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -99,6 +99,8 @@ def validate_inventory_dimension(self): ) def on_submit(self): + self.make_bundle_for_current_qty() + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.repost_future_sle_and_gle() @@ -116,9 +118,52 @@ def on_cancel(self): self.repost_future_sle_and_gle() self.delete_auto_created_batches() + def make_bundle_for_current_qty(self): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + for row in self.items: + if not row.use_serial_batch_fields: + continue + + if row.current_serial_and_batch_bundle: + continue + + if row.current_qty and (row.current_serial_no or row.batch_no): + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.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": row.qty, + "type_of_transaction": "Outward", + "company": self.company, + "is_rejected": 0, + "serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None, + "batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None, + "batch_no": row.batch_no, + "do_not_submit": True, + } + ).make_serial_and_batch_bundle() + + row.current_serial_and_batch_bundle = sn_doc.name + row.db_set( + { + "current_serial_and_batch_bundle": sn_doc.name, + "current_serial_no": "", + "batch_no": "", + } + ) + def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: + if not save and item.use_serial_batch_fields: + continue + if voucher_detail_no and voucher_detail_no != item.name: continue @@ -229,6 +274,9 @@ def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False def set_new_serial_and_batch_bundle(self): for item in self.items: + if item.use_serial_batch_fields: + continue + if not item.qty: continue @@ -291,8 +339,10 @@ def _changed(item): inventory_dimensions_dict=inventory_dimensions_dict, ) - if (item.qty is None or item.qty == item_dict.get("qty")) and ( - item.valuation_rate is None or item.valuation_rate == item_dict.get("rate") + if ( + (item.qty is None or item.qty == item_dict.get("qty")) + and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) + and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) ): return False else: @@ -303,6 +353,11 @@ def _changed(item): if item.valuation_rate is None: item.valuation_rate = item_dict.get("rate") + if item_dict.get("serial_nos"): + item.current_serial_no = item_dict.get("serial_nos") + if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty: + item.serial_no = item.current_serial_no + item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") self.calculate_difference_amount(item, item_dict) @@ -1135,9 +1190,16 @@ def get_stock_balance_for( has_serial_no = bool(item_dict.get("has_serial_no")) has_batch_no = bool(item_dict.get("has_batch_no")) + use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields") + if not batch_no and has_batch_no: # Not enough information to fetch data - return {"qty": 0, "rate": 0, "serial_nos": None} + return { + "qty": 0, + "rate": 0, + "serial_nos": None, + "use_serial_batch_fields": use_serial_batch_fields, + } # TODO: fetch only selected batch's values data = get_stock_balance( @@ -1160,7 +1222,12 @@ def get_stock_balance_for( get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 ) - return {"qty": qty, "rate": rate, "serial_nos": serial_nos} + return { + "qty": qty, + "rate": rate, + "serial_nos": serial_nos, + "use_serial_batch_fields": use_serial_batch_fields, + } @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 0bbfed40d894..479a74af7a86 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1094,7 +1094,7 @@ def create_stock_reconciliation(**args): ) bundle_id = None - if args.batch_no or args.serial_no: + if not args.use_serial_batch_fields and (args.batch_no or args.serial_no): batches = frappe._dict({}) if args.batch_no: batches[args.batch_no] = args.qty @@ -1125,7 +1125,10 @@ def create_stock_reconciliation(**args): "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty, "valuation_rate": args.rate, + "serial_no": args.serial_no if args.use_serial_batch_fields else None, + "batch_no": args.batch_no if args.use_serial_batch_fields else None, "serial_and_batch_bundle": bundle_id, + "use_serial_batch_fields": args.use_serial_batch_fields, }, ) diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index fc4ae6a5fab9..734225972c7f 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -19,11 +19,14 @@ "allow_zero_valuation_rate", "serial_no_and_batch_section", "add_serial_batch_bundle", - "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "column_break_11", + "serial_and_batch_bundle", "current_serial_and_batch_bundle", + "section_break_lypk", "serial_no", + "column_break_eefq", + "batch_no", "section_break_3", "current_qty", "current_amount", @@ -103,10 +106,10 @@ "label": "Serial No and Batch" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Long Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "column_break_11", @@ -171,11 +174,11 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -195,6 +198,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial / Batch Bundle", @@ -204,6 +208,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "current_serial_and_batch_bundle", "fieldtype": "Link", "label": "Current Serial / Batch Bundle", @@ -212,6 +217,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" @@ -222,11 +228,26 @@ "fieldtype": "Link", "label": "Item Group", "options": "Item Group" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_lypk", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_eefq", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2024-01-14 10:04:23.599951", + "modified": "2024-02-04 16:19:44.576022", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py index c82cdf58de1f..1938fec32b0b 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py @@ -26,6 +26,7 @@ class StockReconciliationItem(Document): current_valuation_rate: DF.Currency has_item_scanned: DF.Data | None item_code: DF.Link + item_group: DF.Link | None item_name: DF.Data | None parent: DF.Data parentfield: DF.Data @@ -34,6 +35,7 @@ class StockReconciliationItem(Document): quantity_difference: DF.ReadOnly | None serial_and_batch_bundle: DF.Link | None serial_no: DF.LongText | None + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 40fac4113d6a..3f2c11425528 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -50,6 +50,7 @@ "disable_serial_no_and_batch_selector", "use_naming_series", "naming_series_prefix", + "use_serial_batch_fields", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -420,6 +421,12 @@ "fieldname": "auto_reserve_stock_for_sales_order_on_purchase", "fieldtype": "Check", "label": "Auto Reserve Stock for Sales Order on Purchase" + }, + { + "default": "1", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial / Batch Fields" } ], "icon": "icon-cog", @@ -427,7 +434,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-01-30 14:03:52.143457", + "modified": "2024-02-04 12:01:31.931864", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 088c7cdfe1c3..c4960aa67a85 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -57,6 +57,7 @@ class StockSettings(Document): stock_uom: DF.Link | None update_existing_price_list_rate: DF.Check use_naming_series: DF.Check + use_serial_batch_fields: DF.Check valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] # end: auto-generated types @@ -68,6 +69,7 @@ def validate(self): "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", + "use_serial_batch_fields", ]: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 78df755d7471..d8b5b34d4490 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -794,6 +794,9 @@ def set_other_details(self): setattr(self, "actual_qty", qty) self.__dict__["actual_qty"] = self.actual_qty + if not hasattr(self, "use_serial_batch_fields"): + setattr(self, "use_serial_batch_fields", 0) + def duplicate_package(self): if not self.serial_and_batch_bundle: return @@ -902,9 +905,14 @@ def set_auto_serial_batch_entries_for_outward(self): self.batches = get_available_batches(kwargs) def set_auto_serial_batch_entries_for_inward(self): + print(self.get("serial_nos")) + if (self.get("batches") and self.has_batch_no) or ( self.get("serial_nos") and self.has_serial_no ): + if self.use_serial_batch_fields and self.get("serial_nos"): + self.make_serial_no_if_not_exists() + return self.batch_no = None @@ -916,6 +924,59 @@ def set_auto_serial_batch_entries_for_inward(self): else: self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)}) + def make_serial_no_if_not_exists(self): + non_exists_serial_nos = [] + for row in self.serial_nos: + if not frappe.db.exists("Serial No", row): + non_exists_serial_nos.append(row) + + if non_exists_serial_nos: + self.make_serial_nos(non_exists_serial_nos) + + def make_serial_nos(self, serial_nos): + serial_nos_details = [] + batch_no = None + if self.batches: + batch_no = list(self.batches.keys())[0] + + for serial_no in serial_nos: + 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_name, + self.description, + "Active", + batch_no, + ) + ) + + if serial_nos_details: + fields = [ + "name", + "serial_no", + "creation", + "modified", + "owner", + "modified_by", + "warehouse", + "company", + "item_code", + "item_name", + "description", + "status", + "batch_no", + ] + + frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) + def set_serial_batch_entries(self, doc): if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 017db5d55058..54e0ab5acf80 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -11,6 +11,9 @@ from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_serial_nos, +) from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation from erpnext.stock.valuation import FIFOValuation, LIFOValuation @@ -125,7 +128,21 @@ def get_stock_balance( if with_valuation_rate: if with_serial_no: - serial_nos = get_serial_nos_data_after_transactions(args) + serial_no_details = get_available_serial_nos( + frappe._dict( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": posting_date, + "posting_time": posting_time, + "ignore_warehouse": 1, + } + ) + ) + + serial_nos = "" + if serial_no_details: + serial_nos = "\n".join(d.serial_no for d in serial_no_details) return ( (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) @@ -140,38 +157,6 @@ def get_stock_balance( return last_entry.qty_after_transaction if last_entry else 0.0 -def get_serial_nos_data_after_transactions(args): - - serial_nos = set() - args = frappe._dict(args) - sle = frappe.qb.DocType("Stock Ledger Entry") - - stock_ledger_entries = ( - frappe.qb.from_(sle) - .select("serial_no", "actual_qty") - .where( - (sle.item_code == args.item_code) - & (sle.warehouse == args.warehouse) - & ( - CombineDatetime(sle.posting_date, sle.posting_time) - < CombineDatetime(args.posting_date, args.posting_time) - ) - & (sle.is_cancelled == 0) - ) - .orderby(sle.posting_date, sle.posting_time, sle.creation) - .run(as_dict=1) - ) - - for stock_ledger_entry in stock_ledger_entries: - changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) - if stock_ledger_entry.actual_qty > 0: - serial_nos.update(changed_serial_no) - else: - serial_nos.difference_update(changed_serial_no) - - return "\n".join(serial_nos) - - def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 475b6030780e..8d82709e75f1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -149,6 +149,7 @@ def on_submit(self): self.update_prevdoc_status() self.set_subcontracting_order_status() self.set_consumed_qty_in_subcontract_order() + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.repost_future_sle_and_gle() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 9bfc2fdb7a1f..f9e0a0b591c4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -48,11 +48,14 @@ "reference_name", "section_break_45", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "col_break5", "rejected_serial_and_batch_bundle", - "batch_no", + "section_break_jshh", + "serial_no", "rejected_serial_no", + "column_break_henr", + "batch_no", "manufacture_details", "manufacturer", "column_break_16", @@ -311,22 +314,20 @@ "label": "Serial and Batch Details" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "label": "Serial No", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "eval: !parent.is_return", @@ -478,6 +479,7 @@ "label": "Accounting Details" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -486,6 +488,7 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -546,12 +549,27 @@ "fieldtype": "Check", "label": "Include Exploded Items", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_jshh", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_henr", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 12:05:51.920705", + "modified": "2024-02-04 16:23:30.374865", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index d02160ece451..1a4ce5b977a2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -58,6 +58,7 @@ class SubcontractingReceiptItem(Document): subcontracting_order: DF.Link | None subcontracting_order_item: DF.Data | None subcontracting_receipt_item: DF.Data | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 90bcf4e544ef..957b6a2a654c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -26,10 +26,13 @@ "current_stock", "secbreak_3", "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "col_break4", + "subcontracting_order", + "section_break_zwnh", "serial_no", - "subcontracting_order" + "column_break_qibi", + "batch_no" ], "fields": [ { @@ -60,19 +63,19 @@ "width": "300px" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", "label": "Serial No", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "fieldname": "col_break1", @@ -198,6 +201,7 @@ }, { "columns": 2, + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "in_list_view": 1, @@ -205,12 +209,27 @@ "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_zwnh", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_qibi", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-15 13:55:08.132626", + "modified": "2024-02-04 16:32:17.534162", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py index 2ee55518d52a..8f09197aa83c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py @@ -35,6 +35,7 @@ class SubcontractingReceiptSuppliedItem(Document): serial_no: DF.Text | None stock_uom: DF.Link | None subcontracting_order: DF.Link | None + use_serial_batch_fields: DF.Check # end: auto-generated types pass