Multiple batches for the same item possible?

Good day to you all!

First I want to thank you for this wonderful piece of software having compared multiple ERP systems erpNext is the one that fits our needs the best.

Now to my question:

Our old way of labeling our product batches is as follows

  1. We create a batch e.g. OOP
  2. We manufacture 50 items for this batch OOP001 - OOP050

When we start the next production we will create a new batch for it, in this case OOQ and restart the numbering from 1.

Is this possible with erpNext? I tried today but could only bind one batch to one product.

Thanks in advance!

Paul

You can bind one product with one batch but one batch may contain n numbers of that product.
So let’s say I have a product Macbook Air and I have manufactured 50 quantities of macbook air then I can assign a single batch(ABC1) to that 50 quantities. But if I have another item let’s say Macbook Pro with 50 quantities then I have to assign new batch(ABC2) to them. If you want to track individual item then try serial no.

Thank you for your response.

So it is not possible to have multiple batches per item? It is important for us that every new manufactured batch gets a new char code and restarts the numbering.

Yes it’s possible but you need to complete production order in the chunks based on the batch.

You have to manually enter the new batch number for new finished goods.

I tried that (production order) but i found no way to change the batch.

Do I have to make the production order for say 50 pieces in batch OOP and then after completing it and putting the items into stock I change the batch name in the item?

I’m not at work now but will try again tomorrow

I couldn’t get it to work to my liking so I changed some code and now it does exactly what I want :slight_smile:

Thank you for the support!

@beckerlz80 What did you change? Can you share the code changes?

I changed stock_ledger_entry and serial_no so that when I add manufactured goods to the stock it will check for the last added batch for this item and take the name to generate the new serial numbers.

I want to test the code on the latest version on Monday and will clean it up so I can show it to you :slight_smile:

If you want to take a look now here are the (messy) changes:

serial_no.py:

def validate_serial_no(sle, item_det):
if item_det.has_serial_no==0:
	if sle.serial_no:
		frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
			SerialNoNotRequiredError)
else:
	if sle.serial_no:
		serial_nos = get_serial_nos(sle.serial_no)
		if cint(sle.actual_qty) != flt(sle.actual_qty):
			frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty))

		if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)):
			frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(sle.actual_qty, sle.item_code, len(serial_nos)),
				SerialNoQtyError)

		if len(serial_nos) != len(set(serial_nos)):
			frappe.throw(_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError)

		for serial_no in serial_nos:
			if frappe.db.exists("Serial No", serial_no):
				sr = frappe.get_doc("Serial No", serial_no)

				if sr.item_code!=sle.item_code:
					if not allow_serial_nos_with_different_item(serial_no, sle):
						frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
							sle.item_code), SerialNoItemError)

				if sle.actual_qty > 0 and has_duplicate_serial_no(sr, sle):
					frappe.throw(_("Serial No {0} has already been received").format(serial_no),
						SerialNoDuplicateError)

				if sle.actual_qty < 0:
					if sr.warehouse!=sle.warehouse:
						frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no,
							sle.warehouse), SerialNoWarehouseError)

					if sle.voucher_type in ("Delivery Note", "Sales Invoice") \
						and sle.is_cancelled=="No" and not sr.warehouse:
							frappe.throw(_("Serial No {0} does not belong to any Warehouse")
								.format(serial_no), SerialNoWarehouseError)

			elif sle.actual_qty < 0:
				# transfer out
				frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
	elif sle.actual_qty < 0 or not item_det.serial_no_series:
		if not item_det.serial_no_series:
			batch_det = frappe.db.sql("""select name from tabBatch where item=%s""", sle.item_code, as_dict=True)
			if batch_det:
				item_det.serial_no_series = batch_det[-1].name + ".###";#take the last batch element								
		else:				
			if sle.actual_qty < 0 or not item_det.serial_no_series:
				frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
				SerialNoRequiredError)

def update_serial_nos(sle, item_det):
if not item_det.serial_no_series:
	batch_det = frappe.db.sql("""select name from tabBatch where item=%s""", sle.item_code, as_dict=True)
	if batch_det:
		item_det.serial_no_series = batch_det[-1].name + ".XXX";#take the last batch element
		frappe.msgprint(_("Item {0} not found").format(sle.item_code))
	else:
		frappe.msgprint(_("Item {0} not found 2").format(sle.item_code))
else:
	frappe.msgprint(_("Item {0} not found 3").format(sle.item_code))
	
if sle.is_cancelled == "No" and not sle.serial_no and sle.actual_qty > 0 \
		and item_det.has_serial_no == 1 and item_det.serial_no_series:
	from frappe.model.naming import make_autoname
	serial_nos = []
	for i in xrange(cint(sle.actual_qty)):
		frappe.msgprint(_("Item {0} not found 3").format(item_det.serial_no_series))
		serial_nos.append(make_autoname(item_det.serial_no_series, "Serial No"))
	frappe.db.set(sle, "serial_no", "\n".join(serial_nos))
	validate_serial_no(sle, item_det)

if sle.serial_no:
	serial_nos = get_serial_nos(sle.serial_no)
	for serial_no in serial_nos:
		if frappe.db.exists("Serial No", serial_no):
			sr = frappe.get_doc("Serial No", serial_no)
			sr.via_stock_ledger = True
			sr.item_code = sle.item_code
			sr.warehouse = sle.warehouse if sle.actual_qty > 0 else None
			sr.save(ignore_permissions=True)
		elif sle.actual_qty > 0:
			make_serial_no(serial_no, sle)

stock_ledger_item.py:

def validate_item(self):
	item_det = frappe.db.sql("""select name, has_batch_no, docstatus,
		is_stock_item, has_variants, stock_uom, create_new_batch, serial_no_series, has_serial_no
		from tabItem where name=%s""", self.item_code, as_dict=True)
		
	batch_det = frappe.db.sql("""select name from tabBatch where item=%s""", self.item_code, as_dict=True)
	if not item_det:
		frappe.throw(_("Item {0} not found").format(self.item_code))

	item_det = item_det[0]

	
	if item_det.is_stock_item != 1:
		frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))

	#change batch_no 
	#frappe.throw(_("Serial {0}").format(item_det.serial_no_series))
	#self.batch_no = item_det.serial_no_series.split('.')[0];
	# check if batch number is required
	if self.voucher_type != 'Stock Reconciliation':
		if item_det.has_batch_no ==1:
			#custom
			if not batch_det:
				frappe.throw(_("Item {0} has no batch").format(self.item_code))
			else:
				self.batch_no = batch_det[-1].name;#take the last batch element
			
			if not self.batch_no:
				frappe.throw(_("Batch number is mandatory for Item {0}").format(self.item_code))
			elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
				frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, self.item_code))

		elif item_det.has_batch_no ==0 and self.batch_no:
				frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))

	if item_det.has_variants:
		frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code),
			ItemTemplateCannotHaveStock)

	self.stock_uom = item_det.stock_uom