Custom Script - Serial Number Generator (and batch generator in reply)

This generates a serial number based on set parameters.

Custom Script:
Enter this into your custom script file for ‘Stock Entry’

frappe.ui.form.on("Stock Entry Detail", {
	generate_serial_sequence: function(frm, cdt, cdn) {
		var d = locals[cdt][cdn];
		var serial_number = d.starting_number;
		var serial_array = [];
		var sequence_number = 0;
		if(d.starting_number > d.end_number) {
			msgprint("Starting serial sequence number must not be larger than end number.");
		} else if((d.end_number - d.starting_number) > 100) {
			msgprint("Maximum 100 serial numbers.");
		} else {
		while ( serial_number <= d.end_number) {
    		serial_array.push(d.prefix + serial_number);
			if(!d.serial_no) {
				frappe.model.set_value(d.doctype,, 'serial_no', serial_array[sequence_number]);
			} else{
				frappe.model.set_value(d.doctype,, 'serial_no', d.serial_no + "\n" + serial_array[sequence_number]);
			}			serial_number++;

Custom Fields:
Copy this into a text editor and save as .csv, then use the import tool to upload.

Data Import Template,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Table:,Custom Field,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Please do not change the template headings.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
First data column must be blank.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
"If you are uploading new records, leave the ""name"" (ID) column blank.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
"If you are uploading new records, ""Naming Series"" becomes mandatory, if present.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
"For updating, you can update only selective columns.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
You can only upload upto 5000 records in one go. (may be less in some cases),,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DocType:,Custom Field,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Column Labels:,ID,Document,Field Type,Label,Fieldname,Insert After,Precision,Options,Collapsible,Collapsible Depends On,Default Value,Depends On,Field Description,Permission Level,Width,Is Mandatory Field,Unique,Read Only,Ignore User Permissions,Hidden,Print Hide,Print Hide If No Value,No Copy,Allow on Submit,In Report Filter,In List View,In Standard Filter,Report Hide,Ignore XSS Filter
Column Name:,name,dt,fieldtype,label,fieldname,insert_after,precision,options,collapsible,collapsible_depends_on,default,depends_on,description,permlevel,width,reqd,unique,read_only,ignore_user_permissions,hidden,print_hide,print_hide_if_no_value,no_copy,allow_on_submit,in_filter,in_list_view,in_standard_filter,report_hide,ignore_xss_filter
Type:,Data (text),Link,Select,Data,Data,Select,Select,Text,Check,Code,Text,Code,Text,Int,Data,Check,Check,Check,Check,Check,Check,Check,Check,Check,Check,Check,Check,Check,Check
Info:,,Valid DocType,"One of: Attach, Attach Image, Button, Check, Code, Column Break, Currency, Data, Date, Datetime, Dynamic Link, Float, HTML, Image, Int, Link, Long Text, Password, Percent, Read Only, Section Break, Select, Small Text, Table, Text, Text Editor, Time",,,,"One of: 1, 2, 3, 4, 5, 6, 7, 8, 9",,0 or 1,,,,,Integer,,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1,0 or 1
Start entering data below this line,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,"""Stock Entry Detail-serial_number_generator""",Stock Entry Detail,Section Break,Serial Number Generator,serial_number_generator,transfer_qty,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-prefix""",Stock Entry Detail,Data,Prefix,prefix,serial_number_generator,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-cbsng2""",Stock Entry Detail,Column Break,,cbsng2,end_number,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-starting_number""",Stock Entry Detail,Int,Starting Number,starting_number,cbsng1,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-end_number""",Stock Entry Detail,Int,End Number,end_number,starting_number,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-cbsng1""",Stock Entry Detail,Column Break,,cbsng1,prefix,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,"""Stock Entry Detail-generate_serial_sequence""",Stock Entry Detail,Button,Generate Serial Sequence,generate_serial_sequence,cbsng2,,,0,,,,,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0

This limits you to generating 100 serial numbers at a time, in case of a typo.



is this an issue prevailing in your ERPNext account? Can you share the relevant screenshots along?

Auto generation of serial numbers is already possible through item settings. What’s the use case of the custom script?
Can we auto generate batch numbers in similar way? That will be really useful.

@umair I was just sharing a custom script that I created.

@Mukesh_Variyani The serial number series through item settings is fine, but our company is not creating serials most of the time, but imputing ones they have. One stock entry to the next will likely not be sequential to each other. They need a way to manually specify the serial numbers being recorded.

As for the batch, I have created this script:

//Creates new batches for line items.
frappe.ui.form.on("Purchase Order", {
    validate: function(frm, cdt, cdn) {
        console.log("Batch Creator function triggered");
        var att = [];
        var d = locals[cdt][cdn];
        var ic = [];
        frm.doc.items.forEach(function(d) {
        if (frm.doc.workflow_state == "Submitted") {
// If you would like the batch numbers to start with a number other than 0, change the value of the variable 'i' on the next line.
            for (var i = 0; i < ic.length; i++) {
                console.log("Creating batch for " + ic[i]);
                doc = {
                    "doctype": "Batch",
                    "batch_id": ( + "-" + i),
                    "item": ic[i],

                    "method": "frappe.client.insert",
                    "args": {
                        "doc": doc

This will create batches for PO line items. it should be easily adaptable for other documents. If the PO is 601, then the batches will be 601-0, 601-1, 601-2, etc. for each line item


This script requires a link field called purchase_order in the batch.

The following code in the purchase receipt filters to only allow entry of the batch that matches the purchase order:

cur_frm.fields_dict['items'].grid.get_field('batch_no').get_query = function(doc, cdt, cdn) {
	var d = locals[cdt][cdn];
	return {
		filters: [
			['Batch', 'purchase_order', '=', d.purchase_order],
			['Batch', 'item', '=', d.item_code]

Oh great, Thanks a lot for sharing…:smile:

Hi @cpurbaugh,

Looks like a great script. However, I encountered an error.

Traceback (most recent call last):
File “/home/frappe/frappe-bench/apps/frappe/frappe/”, line 62, in application
response = frappe.handler.handle()
File “/home/frappe/frappe-bench/apps/frappe/frappe/”, line 22, in handle
data = execute_cmd(cmd)
File “/home/frappe/frappe-bench/apps/frappe/frappe/”, line 53, in execute_cmd
return, **frappe.form_dict)
File “/home/frappe/frappe-bench/apps/frappe/frappe/”, line 939, in call
return fn(*args, **newargs)
TypeError: runserverobj() takes at least 1 argument (1 given)

Please advice.


cur_frm.fields_dict[‘items’].grid.get_field(‘batch_no’).get_query = function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return {
filters: [
[‘Batch’, ‘purchase_order’, ‘=’, d.purchase_order],
[‘Batch’, ‘item’, ‘=’, d.item_code]

Hello, how to convert this code into parent? thanks :blush:

Thanks @cpurbaugh, it is really useful.

The CSV format required by the Import Tool is a little different in version 12. Here is the same content with the new format:

ID,Document,Field Type,Label,Fieldname,Insert After,Length,Precision,Options,Fetch From,Fetch If Empty,Collapsible,Collapsible Depends On,Default Value,Depends On,Field Description,Permission Level,Width,Columns,Is Mandatory Field,Unique,Read Only,Ignore User Permissions,Hidden,Print Hide,Print Hide If No Value,Print Width,No Copy,Allow on Submit,In List View,In Standard Filter,In Global Search,Bold,Report Hide,Index,Allow in Quick Entry,Ignore XSS Filter,Translatable
"""Stock Entry Detail-serial_number_generator""",Stock Entry Detail,Section Break,Serial Number Generator,serial_number_generator,transfer_qty,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-prefix""",Stock Entry Detail,Data,Prefix,prefix,serial_number_generator,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-cbsng2""",Stock Entry Detail,Column Break,,cbsng2,end_number,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-starting_number""",Stock Entry Detail,Int,Starting Number,starting_number,cbsng1,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-end_number""",Stock Entry Detail,Int,End Number,end_number,starting_number,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-cbsng1""",Stock Entry Detail,Column Break,,cbsng1,prefix,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,
"""Stock Entry Detail-generate_serial_sequence""",Stock Entry Detail,Button,Generate Serial Sequence,generate_serial_sequence,cbsng2,,,,,,0,,,,,0,,,0,0,0,0,0,0,0,,0,0,0,0,,,0,,,0,

The rest is the same.

Hi @cpurbaugh

I was experiencing a performance issue when generating more than 1000 serial numbers. Find out that the problem is that each time a serial number is generated it is set directly into the serial_no field. To solve this issue is easy, simply set the serial_no field after the serial_array is completed. Here is the code:

generate_serial_sequence: function (frm, cdt, cdn) {
        var d = locals[cdt][cdn];
        var serial_number = d.starting_number;
        var serial_array = [];
        if (d.starting_number > d.end_number) {
            msgprint("La numeración final no debe ser menor que la numeración inicial.");
        } else if ((d.end_number - d.starting_number) > 1000) {
            msgprint("Maximum 1000 serial numbers.");
        } else {
            while (serial_number <= d.end_number) {
                serial_array.push(d.prefix + serial_number);
            if (!d.serial_no) {
                frappe.model.set_value(d.doctype,, 'serial_no', serial_array.join("\n"));
            } else {
                frappe.model.set_value(d.doctype,, 'serial_no', d.serial_no + "\n" + serial_array.join("\n"));