fix: serial and batch selector
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 49ce6b9..3c7c787 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1527,7 +1527,6 @@
ste_doc.load_from_db()
# Create a stock entry to manufacture the item
- print("remove 2 qty from each item")
ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5))
for row in ste_doc.items:
if row.s_warehouse and not row.t_warehouse:
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 382ae2c..6d3af42 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -12,12 +12,12 @@
}
make() {
- let label = this.item?.has_serial_no ? __('Serial No') : __('Batch No');
+ let label = this.item?.has_serial_no ? __('Serial Nos') : __('Batch Nos');
let primary_label = this.bundle
? __('Update') : __('Add');
if (this.item?.has_serial_no && this.item?.batch_no) {
- label = __('Serial No / Batch No');
+ label = __('Serial Nos / Batch Nos');
}
primary_label += ' ' + label;
@@ -26,7 +26,9 @@
title: this.item?.title || primary_label,
fields: this.get_dialog_fields(),
primary_action_label: primary_label,
- primary_action: () => this.update_ledgers()
+ primary_action: () => this.update_ledgers(),
+ secondary_action_label: __('Edit Full Form'),
+ secondary_action: () => this.edit_full_form(),
});
this.dialog.set_value("qty", this.item.qty);
@@ -48,7 +50,7 @@
if (this.item.has_serial_no) {
fields.push({
- fieldtype: 'Link',
+ fieldtype: 'Data',
fieldname: 'scan_serial_no',
label: __('Scan Serial No'),
options: 'Serial No',
@@ -279,6 +281,37 @@
})
}
+ edit_full_form() {
+ let bundle_id = this.item.serial_and_batch_bundle
+ if (!bundle_id) {
+ _new = frappe.model.get_new_doc(
+ "Serial and Batch Bundle", null, null, true
+ );
+
+ _new.item_code = this.item.item_code;
+ _new.warehouse = this.get_warehouse();
+ _new.has_serial_no = this.item.has_serial_no;
+ _new.has_batch_no = this.item.has_batch_no;
+ _new.type_of_transaction = this.get_type_of_transaction();
+ _new.company = this.frm.doc.company;
+ _new.voucher_type = this.frm.doc.doctype;
+ bundle_id = _new.name;
+ }
+
+ frappe.set_route("Form", "Serial and Batch Bundle", bundle_id);
+ this.dialog.hide();
+ }
+
+ get_warehouse() {
+ return (this.item?.outward ?
+ (this.item.warehouse || this.item.s_warehouse)
+ : (this.item.warehouse || this.item.t_warehouse));
+ }
+
+ get_type_of_transaction() {
+ return (this.item?.outward ? 'Outward' : 'Inward');
+ }
+
render_data() {
if (!this.frm.is_new() && this.bundle) {
frappe.call({
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 9e15015..76202ed 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -4,6 +4,7 @@
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt
from frappe.utils.deprecations import deprecated
+from pypika import Order
class DeprecatedSerialNoValuation:
@@ -39,25 +40,25 @@
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company]
for serial_no in invalid_serial_nos:
- incoming_rate = frappe.db.sql(
- """
- select incoming_rate
- from `tabStock Ledger Entry`
- where
- company = %s
- and serial_and_batch_bundle IS NULL
- and actual_qty > 0
- and is_cancelled = 0
- and (serial_no = %s
- or serial_no like %s
- or serial_no like %s
- or serial_no like %s
+ table = frappe.qb.DocType("Stock Ledger Entry")
+ incoming_rate = (
+ frappe.qb.from_(table)
+ .select(table.incoming_rate)
+ .where(
+ (
+ (table.serial_no == serial_no)
+ | (table.serial_no.like(serial_no + "\n%"))
+ | (table.serial_no.like("%\n" + serial_no))
+ | (table.serial_no.like("%\n" + serial_no + "\n%"))
)
- order by posting_date desc
- limit 1
- """,
- (self.sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
- )
+ & (table.company == self.sle.company)
+ & (table.serial_and_batch_bundle.isnull())
+ & (table.actual_qty > 0)
+ & (table.is_cancelled == 0)
+ )
+ .orderby(table.posting_date, order=Order.desc)
+ .limit(1)
+ ).run()
self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0
incoming_values += self.serial_no_incoming_rate[serial_no]
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
index 858b333..b02ad71 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
@@ -8,6 +8,17 @@
refresh(frm) {
frm.trigger('toggle_fields');
+ frm.trigger('prepare_serial_batch_prompt');
+ },
+
+ item_code(frm) {
+ frm.clear_custom_buttons();
+ frm.trigger('prepare_serial_batch_prompt');
+ },
+
+ type_of_transaction(frm) {
+ frm.clear_custom_buttons();
+ frm.trigger('prepare_serial_batch_prompt');
},
warehouse(frm) {
@@ -30,6 +41,91 @@
frm.trigger('toggle_fields');
},
+ prepare_serial_batch_prompt(frm) {
+ if (frm.doc.docstatus === 0 && frm.doc.item_code
+ && frm.doc.type_of_transaction === "Inward") {
+ let label = frm.doc?.has_serial_no === 1
+ ? __('Serial Nos') : __('Batch Nos');
+
+ if (frm.doc?.has_serial_no === 1 && frm.doc?.has_batch_no === 1) {
+ label = __('Serial and Batch Nos');
+ }
+
+ let fields = frm.events.get_prompt_fields(frm);
+
+ frm.add_custom_button(__("Make " + label), () => {
+ frappe.prompt(fields, (data) => {
+ frm.events.add_serial_batch(frm, data);
+ }, "Add " + label, "Make " + label);
+ });
+ }
+ },
+
+ get_prompt_fields(frm) {
+ let attach_field = {
+ "label": __("Attach CSV File"),
+ "fieldname": "csv_file",
+ "fieldtype": "Attach"
+ }
+
+ if (!frm.doc.has_batch_no) {
+ attach_field.depends_on = "eval:doc.using_csv_file === 1"
+ }
+
+ let fields = [
+ {
+ "label": __("Using CSV File"),
+ "fieldname": "using_csv_file",
+ "default": 1,
+ "fieldtype": "Check",
+ },
+ attach_field,
+ {
+ "fieldtype": "Section Break",
+ }
+ ]
+
+ if (frm.doc.has_serial_no) {
+ fields.push({
+ "label": "Serial Nos",
+ "fieldname": "serial_nos",
+ "fieldtype": "Small Text",
+ "depends_on": "eval:doc.using_csv_file === 0"
+ })
+ }
+
+ if (frm.doc.has_batch_no) {
+ fields = attach_field
+ }
+
+ return fields;
+ },
+
+ add_serial_batch(frm, prompt_data) {
+ frm.events.validate_prompt_data(frm, prompt_data);
+
+ frm.call({
+ method: "add_serial_batch",
+ doc: frm.doc,
+ args: {
+ "data": prompt_data,
+ },
+ callback(r) {
+ refresh_field("entries");
+ }
+ });
+ },
+
+ validate_prompt_data(frm, prompt_data) {
+ if (prompt_data.using_csv_file && !prompt_data.csv_file) {
+ frappe.throw(__("Please attach CSV file"));
+ }
+
+ if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
+ frappe.throw(__("Please enter serial nos"));
+ }
+ },
+
toggle_fields(frm) {
frm.fields_dict.entries.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no
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 77ba13a..6955c76 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
@@ -9,13 +9,13 @@
"item_details_tab",
"naming_series",
"company",
- "warehouse",
- "type_of_transaction",
- "column_break_4",
- "item_code",
"item_name",
"has_serial_no",
"has_batch_no",
+ "column_break_4",
+ "item_code",
+ "warehouse",
+ "type_of_transaction",
"serial_no_and_batch_no_tab",
"entries",
"quantity_and_rate_section",
@@ -84,7 +84,8 @@
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
- "label": "Item Name"
+ "label": "Item Name",
+ "read_only": 1
},
{
"default": "0",
@@ -243,7 +244,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-04-06 02:35:38.404537",
+ "modified": "2023-04-10 20:02:42.964309",
"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 0b7eda9..f787caa 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
@@ -2,6 +2,7 @@
# For license information, please see license.txt
import collections
+import csv
from collections import defaultdict
from typing import Dict, List
@@ -9,7 +10,17 @@
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import add_days, cint, flt, get_link_to_form, nowtime, today
+from frappe.utils import (
+ add_days,
+ cint,
+ cstr,
+ flt,
+ get_link_to_form,
+ now,
+ nowtime,
+ parse_json,
+ today,
+)
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
@@ -626,6 +637,173 @@
self.delink_reference_from_batch()
self.clear_table()
+ @frappe.whitelist()
+ def add_serial_batch(self, data):
+ serial_nos, batch_nos = [], []
+ if isinstance(data, str):
+ data = parse_json(data)
+
+ if data.get("csv_file"):
+ serial_nos, batch_nos = get_serial_batch_from_csv(self.item_code, data.get("csv_file"))
+ else:
+ serial_nos, batch_nos = get_serial_batch_from_data(self.item_code, data)
+
+ if not serial_nos and not batch_nos:
+ return
+
+ if serial_nos:
+ self.set("entries", serial_nos)
+ elif batch_nos:
+ self.set("entries", batch_nos)
+
+
+def get_serial_batch_from_csv(item_code, file_path):
+ file_path = frappe.get_site_path() + file_path
+ serial_nos = []
+ batch_nos = []
+
+ with open(file_path, "r") as f:
+ reader = csv.reader(f)
+ serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(reader)
+
+ if serial_nos:
+ make_serial_nos(item_code, serial_nos)
+
+ print(batch_nos)
+ if batch_nos:
+ make_batch_nos(item_code, batch_nos)
+
+ return serial_nos, batch_nos
+
+
+def parse_csv_file_to_get_serial_batch(reader):
+ has_serial_no, has_batch_no = False, False
+ serial_nos = []
+ batch_nos = []
+
+ for index, row in enumerate(reader):
+ if index == 0:
+ has_serial_no = row[0] == "Serial No"
+ has_batch_no = row[0] == "Batch No"
+ continue
+
+ if not row[0]:
+ continue
+
+ if has_serial_no or (has_serial_no and has_batch_no):
+ _dict = {"serial_no": row[0], "qty": 1}
+
+ if has_batch_no:
+ _dict.update(
+ {
+ "batch_no": row[1],
+ "qty": row[2],
+ }
+ )
+
+ serial_nos.append(_dict)
+ elif has_batch_no:
+ batch_nos.append(
+ {
+ "batch_no": row[0],
+ "qty": row[1],
+ }
+ )
+
+ return serial_nos, batch_nos
+
+
+def get_serial_batch_from_data(item_code, kwargs):
+ serial_nos = []
+ batch_nos = []
+ if kwargs.get("serial_nos"):
+ data = parse_serial_nos(kwargs.get("serial_nos"))
+ for serial_no in data:
+ if not serial_no:
+ continue
+ serial_nos.append({"serial_no": serial_no, "qty": 1})
+
+ make_serial_nos(item_code, serial_nos)
+
+ return serial_nos, batch_nos
+
+
+def make_serial_nos(item_code, serial_nos):
+ item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
+
+ serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
+
+ serial_nos_details = []
+ user = frappe.session.user
+ for serial_no in serial_nos:
+ serial_nos_details.append(
+ (
+ serial_no,
+ serial_no,
+ now(),
+ now(),
+ user,
+ user,
+ item.item_code,
+ item.item_name,
+ item.description,
+ "Inactive",
+ )
+ )
+
+ fields = [
+ "name",
+ "serial_no",
+ "creation",
+ "modified",
+ "owner",
+ "modified_by",
+ "item_code",
+ "item_name",
+ "description",
+ "status",
+ ]
+
+ frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
+ frappe.msgprint(_("Serial Nos are created successfully"))
+
+
+def make_batch_nos(item_code, batch_nos):
+ item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
+
+ batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")]
+
+ batch_nos_details = []
+ user = frappe.session.user
+ for batch_no in batch_nos:
+ batch_nos_details.append(
+ (batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
+ )
+
+ fields = [
+ "name",
+ "batch_id",
+ "creation",
+ "modified",
+ "owner",
+ "modified_by",
+ "item",
+ "item_name",
+ "description",
+ ]
+
+ frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
+
+ frappe.msgprint(_("Batch Nos are created successfully"))
+
+
+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()]
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -690,13 +868,13 @@
@frappe.whitelist()
def add_serial_batch_ledgers(entries, child_row, doc) -> object:
if isinstance(child_row, str):
- child_row = frappe._dict(frappe.parse_json(child_row))
+ child_row = frappe._dict(parse_json(child_row))
if isinstance(entries, str):
- entries = frappe.parse_json(entries)
+ entries = parse_json(entries)
if doc and isinstance(doc, str):
- parent_doc = frappe.parse_json(doc)
+ parent_doc = parse_json(doc)
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc)