From 708eefb3835b7bf300bff2ae82002dfb6c47fa98 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 10 Jul 2023 16:54:55 +0530 Subject: [PATCH] fix: Added report 'Serial and Batch Summary' to view serial / batch nos --- erpnext/public/js/controllers/transaction.js | 2 + erpnext/public/js/utils.js | 19 +- .../serial_and_batch_bundle.json | 4 +- .../serial_and_batch_bundle.py | 13 +- .../stock/doctype/stock_entry/stock_entry.js | 1 + .../stock_reconciliation.js | 1 + .../serial_and_batch_summary/__init__.py | 0 .../serial_and_batch_summary.js | 95 +++++++ .../serial_and_batch_summary.json | 38 +++ .../serial_and_batch_summary.py | 245 ++++++++++++++++++ 10 files changed, 411 insertions(+), 7 deletions(-) create mode 100644 erpnext/stock/report/serial_and_batch_summary/__init__.py create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 543d0e9790..6410333f0c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -358,12 +358,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } refresh() { + erpnext.toggle_naming_series(); erpnext.hide_company(); this.set_dynamic_labels(); this.setup_sms(); this.setup_quality_inspection(); this.validate_has_items(); + erpnext.utils.view_serial_batch_nos(this.frm); } scan_barcode() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a859a671b0..29c8aa0fa0 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -113,6 +113,23 @@ $.extend(erpnext.utils, { } }, + view_serial_batch_nos: function(frm) { + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); + + if (bundle_ids?.length) { + frm.add_custom_button(__('Serial / Batch Nos'), () => { + frappe.route_options = { + "voucher_no": frm.doc.name, + "voucher_type": frm.doc.doctype, + "from_date": frm.doc.posting_date || frm.doc.transaction_date, + "to_date": frm.doc.posting_date || frm.doc.transaction_date, + "company": frm.doc.company, + }; + frappe.set_route("query-report", "Serial and Batch Summary"); + }, __('View')); + } + }, + add_indicator_for_multicompany: function(frm, info) { frm.dashboard.stats_area.show(); frm.dashboard.stats_area_row.addClass('flex'); @@ -1011,4 +1028,4 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) { $btn.on("click", function() { context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true); }); -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 6955c761e1..c5b96ff0fe 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -193,7 +193,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Naming Series", - "options": "SBB-.####" + "options": "SABB-.########" }, { "default": "0", @@ -244,7 +244,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-10 20:02:42.964309", + "modified": "2023-07-16 10:53:04.045605", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0c6d33bae2..43bd7ac78c 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 @@ -889,13 +889,16 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() -def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None): - filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name) +def get_serial_batch_ledgers(item_code=None, docstatus=None, voucher_no=None, name=None): + filters = get_filters_for_bundle( + item_code=item_code, docstatus=docstatus, voucher_no=voucher_no, name=name + ) return frappe.get_all( "Serial and Batch Bundle", fields=[ "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`item_code`", "`tabSerial and Batch Entry`.`qty`", "`tabSerial and Batch Entry`.`warehouse`", "`tabSerial and Batch Entry`.`batch_no`", @@ -906,12 +909,14 @@ def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=No ) -def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None): +def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name=None): filters = [ - ["Serial and Batch Bundle", "item_code", "=", item_code], ["Serial and Batch Bundle", "is_cancelled", "=", 0], ] + if item_code: + filters.append(["Serial and Batch Bundle", "item_code", "=", item_code]) + if not docstatus: docstatus = [0, 1] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 403e04ae60..3e83fafcad 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -925,6 +925,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0664c2929c..cb2adf1682 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -337,6 +337,7 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st refresh() { if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/report/serial_and_batch_summary/__init__.py b/erpnext/stock/report/serial_and_batch_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js new file mode 100644 index 0000000000..10e5925ff4 --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js @@ -0,0 +1,95 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Serial and Batch Summary"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today() + }, + { + "fieldname":"item_code", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + }, + { + "fieldname":"voucher_type", + "label": __("Voucher Type"), + "fieldtype": "Link", + "options": "DocType", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_voucher_type", + } + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let voucher_type = frappe.query_report.get_filter_value('voucher_type'); + if (!voucher_type) return; + + return frappe.db.get_link_options(voucher_type, txt); + }, + }, + { + "fieldname":"serial_no", + "label": __("Serial No"), + "fieldtype": "Link", + "options": "Serial No", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_serial_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + }, + { + "fieldname":"batch_no", + "label": __("Batch No"), + "fieldtype": "Link", + "options": "Batch", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_batch_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + } + ] +}; diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json new file mode 100644 index 0000000000..7511e3a198 --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-07-13 16:53:27.735091", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2023-07-13 16:53:33.204591", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial and Batch Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial and Batch Bundle", + "report_name": "Serial and Batch Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock User" + }, + { + "role": "Maintenance User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py new file mode 100644 index 0000000000..3ea5e8278d --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -0,0 +1,245 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters, data) + + return columns, data + + +def get_data(filters): + filter_conditions = get_filter_conditions(filters) + + return frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Bundle`.`voucher_type`", + "`tabSerial and Batch Bundle`.`posting_date`", + "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`company`", + "`tabSerial and Batch Bundle`.`voucher_no`", + "`tabSerial and Batch Bundle`.`item_code`", + "`tabSerial and Batch Bundle`.`item_name`", + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`warehouse`", + "`tabSerial and Batch Entry`.`incoming_rate`", + "`tabSerial and Batch Entry`.`stock_value_difference`", + "`tabSerial and Batch Entry`.`qty`", + ], + filters=filter_conditions, + order_by="posting_date", + ) + + +def get_filter_conditions(filters): + filter_conditions = [ + ["Serial and Batch Bundle", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ] + + for field in ["voucher_type", "voucher_no", "item_code", "warehouse", "company"]: + if filters.get(field): + if field == "voucher_no": + filter_conditions.append(["Serial and Batch Bundle", field, "in", filters.get(field)]) + else: + filter_conditions.append(["Serial and Batch Bundle", field, "=", filters.get(field)]) + + if filters.get("from_date") and filters.get("to_date"): + filter_conditions.append( + [ + "Serial and Batch Bundle", + "posting_date", + "between", + [filters.get("from_date"), filters.get("to_date")], + ] + ) + + for field in ["serial_no", "batch_no"]: + if filters.get(field): + filter_conditions.append(["Serial and Batch Entry", field, "=", filters.get(field)]) + + return filter_conditions + + +def get_columns(filters, data): + columns = [ + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, + { + "label": _("Serial and Batch Bundle"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Serial and Batch Bundle", + "width": 110, + }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + ] + + item_details = {} + + item_codes = [] + if filters.get("voucher_type"): + item_codes = [d.item_code for d in data] + + if filters.get("item_code") or (item_codes and len(list(set(item_codes))) == 1): + item_details = frappe.get_cached_value( + "Item", + filters.get("item_code") or item_codes[0], + ["has_serial_no", "has_batch_no"], + as_dict=True, + ) + + if not filters.get("voucher_no"): + columns.extend( + [ + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "fieldtype": "Link", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 160, + }, + ] + ) + + if not filters.get("item_code"): + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if not filters.get("warehouse"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + } + ) + + if not item_details or item_details.get("has_serial_no"): + columns.append( + {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Data", "width": 120} + ) + + if not item_details or item_details.get("has_batch_no"): + columns.extend( + [ + {"label": _("Batch No"), "fieldname": "batch_no", "fieldtype": "Data", "width": 120}, + {"label": _("Batch Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + ] + ) + + columns.extend( + [ + {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Float", "width": 120}, + { + "label": _("Change in Stock Value"), + "fieldname": "stock_value_difference", + "fieldtype": "Float", + "width": 120, + }, + ] + ) + + return columns + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_voucher_type(doctype, txt, searchfield, start, page_len, filters): + child_doctypes = frappe.get_all( + "DocField", + filters={"fieldname": "serial_and_batch_bundle"}, + fields=["distinct parent as parent"], + ) + + query_filters = {"options": ["in", [d.parent for d in child_doctypes]]} + if txt: + query_filters["parent"] = ["like", "%{}%".format(txt)] + + return frappe.get_all("DocField", filters=query_filters, fields=["distinct parent"], as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_serial_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["serial_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["serial_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["serial_no"], as_list=True + ) + + else: + query_filters["item_code"] = filters.get("item_code") + return frappe.get_all("Serial No", filters=query_filters, as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_batch_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["batch_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["batch_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["batch_no"], as_list=True + ) + + else: + query_filters["item"] = filters.get("item_code") + return frappe.get_all("Batch", filters=query_filters, as_list=True)