refactor: serial no normalization
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index befde71..6156aba 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
+from frappe.utils import cint, flt, get_link_to_form, getdate
import erpnext
from erpnext.accounts.general_ledger import (
@@ -328,26 +328,49 @@
def make_batches(self, warehouse_field):
"""Create batches if required. Called before submit"""
for d in self.items:
- if d.get(warehouse_field) and not d.batch_no:
+ if d.get(warehouse_field) and not d.serial_and_batch_bundle:
has_batch_no, create_new_batch = frappe.get_cached_value(
"Item", d.item_code, ["has_batch_no", "create_new_batch"]
)
if has_batch_no and create_new_batch:
- d.batch_no = (
+ batch_no = (
frappe.get_doc(
- dict(
- doctype="Batch",
- item=d.item_code,
- supplier=getattr(self, "supplier", None),
- reference_doctype=self.doctype,
- reference_name=self.name,
- )
+ dict(doctype="Batch", item=d.item_code, supplier=getattr(self, "supplier", None))
)
.insert()
.name
)
+ d.serial_and_batch_bundle = (
+ frappe.get_doc(
+ {
+ "doctype": "Serial and Batch Bundle",
+ "item_code": d.item_code,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "ledgers": [
+ {
+ "batch_no": batch_no,
+ "qty": d.qty,
+ "warehouse": d.get(warehouse_field),
+ }
+ ],
+ }
+ )
+ .submit()
+ .name
+ )
+
+ frappe.db.set_value(
+ "Batch",
+ batch_no,
+ {
+ "reference_doctype": "Serial and Batch Bundle",
+ "reference_name": d.serial_and_batch_bundle,
+ },
+ )
+
def check_expense_account(self, item):
if not item.get("expense_account"):
msg = _("Please set an Expense Account in the Items table")
@@ -387,27 +410,20 @@
)
def delete_auto_created_batches(self):
- for d in self.items:
- if not d.batch_no:
- continue
+ for row in self.items:
+ if row.serial_and_batch_bundle:
+ frappe.db.set_value(
+ "Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
+ )
- frappe.db.set_value(
- "Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None
- )
-
- d.batch_no = None
- d.db_set("batch_no", None)
-
- for data in frappe.get_all(
- "Batch", {"reference_name": self.name, "reference_doctype": self.doctype}
- ):
- frappe.delete_doc("Batch", data.name)
+ row.db_set("serial_and_batch_bundle", None)
def get_sl_entries(self, d, args):
sl_dict = frappe._dict(
{
"item_code": d.get("item_code", None),
"warehouse": d.get("warehouse", None),
+ "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0],
@@ -420,7 +436,6 @@
),
"incoming_rate": 0,
"company": self.company,
- "batch_no": cstr(d.get("batch_no")).strip(),
"serial_no": d.get("serial_no"),
"project": d.get("project") or self.get("project"),
"is_cancelled": 1 if self.docstatus == 2 else 0,
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index b0e08cc..e37a9b7 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -341,10 +341,36 @@
}
frappe.throw(msg);
}
- });
-
- }
+ }
+ );
}
+ }
+
+ update_serial_batch_bundle(doc, cdt, cdn) {
+ let item = locals[cdt][cdn];
+ let me = this;
+ let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
+
+ frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
+ .then((r) => {
+ if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
+ item.has_serial_no = r.message.has_serial_no;
+ item.has_batch_no = r.message.has_batch_no;
+
+ frappe.require(path, function() {
+ new erpnext.SerialNoBatchBundleUpdate(
+ me.frm, item, (r) => {
+ if (r) {
+ me.frm.refresh_fields();
+ frappe.model.set_value(cdt, cdn,
+ "serial_and_batch_bundle", r.name);
+ }
+ }
+ );
+ });
+ }
+ });
+ }
};
cur_frm.add_fetch('project', 'cost_center', 'cost_center');
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 96ff44e..b4676c1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -119,9 +119,14 @@
}
});
- if(this.frm.fields_dict["items"].grid.get_field('batch_no')) {
- this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
- return me.set_query_for_batch(doc, cdt, cdn);
+ if(this.frm.fields_dict["items"].grid.get_field('serial_and_batch_bundle')) {
+ this.frm.set_query("serial_and_batch_bundle", "items", function(doc, cdt, cdn) {
+ let item_row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'item_code': item_row.item_code
+ }
+ }
});
}
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 64c5ee5..1c98037 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -616,3 +616,195 @@
}
//# sourceURL=serial_no_batch_selector.js
+
+
+erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
+ constructor(frm, item, callback) {
+ this.frm = frm;
+ this.item = item;
+ this.qty = item.qty;
+ this.callback = callback;
+ this.make();
+ this.render_data();
+ }
+
+ make() {
+ this.dialog = new frappe.ui.Dialog({
+ title: __('Update Serial No / Batch No'),
+ fields: this.get_dialog_fields(),
+ primary_action_label: __('Update'),
+ primary_action: () => this.update_ledgers()
+ });
+ this.dialog.show();
+ }
+
+ get_serial_no_filters() {
+ return {
+ 'item_code': this.item.item_code,
+ 'warehouse': ["=", ""],
+ 'delivery_document_no': ["=", ""],
+ };
+ }
+
+ get_dialog_fields() {
+ let fields = [];
+
+ if (this.item.has_serial_no) {
+ fields.push({
+ fieldtype: 'Link',
+ fieldname: 'scan_serial_no',
+ label: __('Scan Serial No'),
+ options: 'Serial No',
+ get_query: () => {
+ return {
+ filters: this.get_serial_no_filters()
+ };
+ },
+ onchange: () => this.update_serial_batch_no()
+ });
+ }
+
+ if (this.item.has_batch_no && this.item.has_serial_no) {
+ fields.push({
+ fieldtype: 'Column Break',
+ label: __('Batch No')
+ });
+ }
+
+ if (this.item.has_batch_no) {
+ fields.push({
+ fieldtype: 'Link',
+ fieldname: 'scan_batch_no',
+ label: __('Scan Batch No'),
+ options: 'Batch',
+ onchange: () => this.update_serial_batch_no()
+ });
+ }
+
+ if (this.item.has_batch_no && this.item.has_serial_no) {
+ fields.push({
+ fieldtype: 'Section Break',
+ });
+ }
+
+ fields.push({
+ fieldname: 'ledgers',
+ fieldtype: 'Table',
+ allow_bulk_edit: true,
+ data: [],
+ fields: this.get_dialog_table_fields(),
+ });
+
+ return fields;
+ }
+
+ get_dialog_table_fields() {
+ let fields = []
+
+ if (this.item.has_serial_no) {
+ fields.push({
+ fieldtype: 'Link',
+ options: 'Serial No',
+ fieldname: 'serial_no',
+ label: __('Serial No'),
+ in_list_view: 1,
+ get_query: () => {
+ return {
+ filters: this.get_serial_no_filters()
+ }
+ }
+ })
+ } else if (this.item.has_batch_no) {
+ fields = [
+ {
+ fieldtype: 'Link',
+ options: 'Batch',
+ fieldname: 'batch_no',
+ label: __('Batch No'),
+ in_list_view: 1,
+ },
+ {
+ fieldtype: 'Float',
+ fieldname: 'qty',
+ label: __('Quantity'),
+ in_list_view: 1,
+ }
+ ]
+ }
+
+ fields.push({
+ fieldtype: 'Data',
+ fieldname: 'name',
+ label: __('Name'),
+ hidden: 1,
+ })
+
+ return fields;
+ }
+
+ update_serial_batch_no() {
+ const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
+
+ if (scan_serial_no) {
+ this.dialog.fields_dict.ledgers.df.data.push({
+ serial_no: scan_serial_no
+ });
+
+ this.dialog.fields_dict.scan_serial_no.set_value('');
+ } else if (scan_batch_no) {
+ this.dialog.fields_dict.ledgers.df.data.push({
+ batch_no: scan_batch_no
+ });
+
+ this.dialog.fields_dict.scan_batch_no.set_value('');
+ }
+
+ this.dialog.fields_dict.ledgers.grid.refresh();
+ }
+
+ update_ledgers() {
+ if (!this.frm.is_new()) {
+ let ledgers = this.dialog.get_values().ledgers;
+
+ if (ledgers && !ledgers.length) {
+ frappe.throw(__('Please add atleast one Serial No / Batch No'));
+ }
+
+ frappe.call({
+ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_no_ledgers',
+ args: {
+ ledgers: ledgers,
+ child_row: this.item
+ }
+ }).then(r => {
+ this.callback && this.callback(r.message);
+ this.dialog.hide();
+ })
+ }
+ }
+
+ render_data() {
+ if (!this.frm.is_new() && this.item.serial_and_batch_bundle) {
+ frappe.call({
+ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_no_ledgers',
+ args: {
+ item_code: this.item.item_code,
+ name: this.item.serial_and_batch_bundle,
+ voucher_no: this.item.parent,
+ }
+ }).then(r => {
+ if (r.message) {
+ this.set_data(r.message);
+ }
+ })
+ }
+ }
+
+ set_data(data) {
+ data.forEach(d => {
+ this.dialog.fields_dict.ledgers.df.data.push(d);
+ });
+
+ this.dialog.fields_dict.ledgers.grid.refresh();
+ }
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/package_item/__init__.py b/erpnext/stock/doctype/package_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/__init__.py
diff --git a/erpnext/stock/doctype/package_item/package_item.js b/erpnext/stock/doctype/package_item/package_item.js
new file mode 100644
index 0000000..65fda46
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Item', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/package_item/package_item.json b/erpnext/stock/doctype/package_item/package_item.json
new file mode 100644
index 0000000..5b0246f
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.json
@@ -0,0 +1,138 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:56:38.338267",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_details_tab",
+ "company",
+ "item_code",
+ "column_break_4",
+ "warehouse",
+ "qty",
+ "serial_no_and_batch_no_tab",
+ "transactions",
+ "reference_details_tab",
+ "voucher_type",
+ "voucher_no",
+ "column_break_12",
+ "voucher_detail_no",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Package Item",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "item_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Warehouse",
+ "options": "Warehouse",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Total Qty"
+ },
+ {
+ "fieldname": "reference_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Reference Details"
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "voucher_detail_no",
+ "fieldtype": "Data",
+ "label": "Voucher Detail No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "serial_no_and_batch_no_tab",
+ "fieldtype": "Tab Break",
+ "label": "Serial No and Batch No"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "transactions",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "Serial and Batch No Transaction",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-10-06 22:07:31.732744",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Package Item",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/package_item/package_item.py b/erpnext/stock/doctype/package_item/package_item.py
new file mode 100644
index 0000000..c0a2eaa
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class PackageItem(Document):
+ pass
diff --git a/erpnext/stock/doctype/package_item/test_package_item.py b/erpnext/stock/doctype/package_item/test_package_item.py
new file mode 100644
index 0000000..6dcc9cb
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/test_package_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestPackageItem(FrappeTestCase):
+ pass
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 312c166..e0cb8ca 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -7,6 +7,8 @@
frappe.ui.form.on("Purchase Receipt", {
setup: (frm) => {
+ frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
+
frm.make_methods = {
'Landed Cost Voucher': () => {
let lcv = frappe.model.get_new_doc('Landed Cost Voucher');
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 3373d8a..660504d 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -283,7 +283,12 @@
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Serial and Batch Bundle",
+ )
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
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 cd320fd..97e7d72 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -91,14 +91,12 @@
"delivery_note_item",
"putaway_rule",
"section_break_45",
- "allow_zero_valuation_rate",
- "bom",
- "serial_no",
+ "update_serial_batch_bundle",
+ "serial_and_batch_bundle",
"col_break5",
+ "allow_zero_valuation_rate",
"include_exploded_items",
- "batch_no",
- "rejected_serial_no",
- "item_tax_rate",
+ "bom",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -110,6 +108,7 @@
"manufacturer_part_no",
"accounting_details_section",
"expense_account",
+ "item_tax_rate",
"column_break_102",
"provisional_expense_account",
"accounting_dimensions_section",
@@ -565,37 +564,8 @@
},
{
"fieldname": "section_break_45",
- "fieldtype": "Section Break"
- },
- {
- "depends_on": "eval:!doc.is_fixed_asset",
- "fieldname": "serial_no",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Serial No",
- "no_copy": 1,
- "oldfieldname": "serial_no",
- "oldfieldtype": "Text"
- },
- {
- "depends_on": "eval:!doc.is_fixed_asset",
- "fieldname": "batch_no",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Batch No",
- "no_copy": 1,
- "oldfieldname": "batch_no",
- "oldfieldtype": "Link",
- "options": "Batch",
- "print_hide": 1
- },
- {
- "depends_on": "eval:!doc.is_fixed_asset",
- "fieldname": "rejected_serial_no",
- "fieldtype": "Small Text",
- "label": "Rejected Serial No",
- "no_copy": 1,
- "print_hide": 1
+ "fieldtype": "Section Break",
+ "label": "Serial and Batch No"
},
{
"fieldname": "item_tax_template",
@@ -1016,12 +986,23 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": "Serial and Batch Bundle",
+ "options": "Serial and Batch Bundle"
+ },
+ {
+ "fieldname": "update_serial_batch_bundle",
+ "fieldtype": "Button",
+ "label": "Add Serial / Batch No"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-02-28 15:43:04.470104",
+ "modified": "2023-02-28 16:43:04.470104",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py b/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py
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
new file mode 100644
index 0000000..085e33d
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
@@ -0,0 +1,80 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Serial and Batch Bundle', {
+ setup(frm) {
+ frm.trigger('set_queries');
+ },
+
+ refresh(frm) {
+ frm.trigger('toggle_fields');
+ },
+
+ set_queries(frm) {
+ frm.set_query('item_code', () => {
+ return {
+ query: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.item_query',
+ };
+ });
+
+ frm.set_query('voucher_type', () => {
+ return {
+ filters: {
+ 'istable': 0,
+ 'issingle': 0,
+ 'is_submittable': 1,
+ }
+ };
+ });
+
+ frm.set_query('voucher_no', () => {
+ return {
+ filters: {
+ 'docstatus': ["!=", 2],
+ }
+ };
+ });
+
+ frm.set_query('serial_no', 'ledgers', () => {
+ return {
+ filters: {
+ item_code: frm.doc.item_code,
+ }
+ };
+ });
+
+ frm.set_query('batch_no', 'ledgers', () => {
+ return {
+ filters: {
+ item: frm.doc.item_code,
+ }
+ };
+ });
+
+ frm.set_query('warehouse', 'ledgers', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ }
+ };
+ });
+ },
+
+ has_serial_no(frm) {
+ frm.trigger('toggle_fields');
+ },
+
+ has_batch_no(frm) {
+ frm.trigger('toggle_fields');
+ },
+
+ toggle_fields(frm) {
+ frm.fields_dict.ledgers.grid.update_docfield_property(
+ 'serial_no', 'read_only', !frm.doc.has_serial_no
+ );
+
+ frm.fields_dict.ledgers.grid.update_docfield_property(
+ 'batch_no', 'read_only', !frm.doc.has_batch_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
new file mode 100644
index 0000000..a08ed83
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -0,0 +1,162 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:56:38.338267",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_details_tab",
+ "company",
+ "item_group",
+ "has_serial_no",
+ "column_break_4",
+ "item_code",
+ "item_name",
+ "has_batch_no",
+ "serial_no_and_batch_no_tab",
+ "ledgers",
+ "qty",
+ "tab_break_12",
+ "voucher_type",
+ "voucher_no",
+ "is_cancelled",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "item_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_serial_no",
+ "fieldname": "has_serial_no",
+ "fieldtype": "Check",
+ "label": "Has Serial No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_batch_no",
+ "fieldname": "has_batch_no",
+ "fieldtype": "Check",
+ "label": "Has Batch No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "serial_no_and_batch_no_tab",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "ledgers",
+ "fieldtype": "Table",
+ "label": "Serial / Batch Ledgers",
+ "options": "Serial and Batch Ledger",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Total Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_cancelled",
+ "fieldtype": "Check",
+ "label": "Is Cancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Serial and Batch Bundle",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "tab_break_12",
+ "fieldtype": "Tab Break",
+ "label": "Reference"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-11-24 13:05:11.623968",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Serial and Batch Bundle",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "item_code"
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..ae25aad
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class SerialandBatchBundle(Document):
+ def validate(self):
+ self.validate_serial_and_batch_no()
+
+ def validate_serial_and_batch_no(self):
+ if self.item_code and not self.has_serial_no and not self.has_batch_no:
+ msg = f"The Item {self.item_code} does not have Serial No or Batch No"
+ frappe.throw(_(msg))
+
+ def before_cancel(self):
+ self.delink_serial_and_batch_bundle()
+ self.clear_table()
+
+ def delink_serial_and_batch_bundle(self):
+ self.voucher_no = None
+
+ sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name})
+
+ for sle in sles:
+ frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None)
+
+ def clear_table(self):
+ self.set("ledgers", [])
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
+ item_filters = {"disabled": 0}
+ if txt:
+ item_filters["name"] = ("like", f"%{txt}%")
+
+ return frappe.get_all(
+ "Item",
+ filters=item_filters,
+ or_filters={"has_serial_no": 1, "has_batch_no": 1},
+ fields=["name", "item_name"],
+ as_list=1,
+ )
+
+
+@frappe.whitelist()
+def get_serial_batch_no_ledgers(item_code, voucher_no, name=None):
+ return frappe.get_all(
+ "Serial and Batch Bundle",
+ fields=[
+ "`tabSerial and Batch Ledger`.`name`",
+ "`tabSerial and Batch Ledger`.`qty`",
+ "`tabSerial and Batch Ledger`.`warehouse`",
+ "`tabSerial and Batch Ledger`.`batch_no`",
+ "`tabSerial and Batch Ledger`.`serial_no`",
+ ],
+ filters=[
+ ["Serial and Batch Bundle", "item_code", "=", item_code],
+ ["Serial and Batch Ledger", "parent", "=", name],
+ ["Serial and Batch Bundle", "voucher_no", "=", voucher_no],
+ ["Serial and Batch Bundle", "docstatus", "!=", 2],
+ ],
+ )
+
+
+@frappe.whitelist()
+def add_serial_batch_no_ledgers(ledgers, child_row) -> object:
+ if isinstance(child_row, str):
+ child_row = frappe._dict(frappe.parse_json(child_row))
+
+ if isinstance(ledgers, str):
+ ledgers = frappe.parse_json(ledgers)
+
+ if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
+ doc = update_serial_batch_no_ledgers(ledgers, child_row)
+ else:
+ doc = create_serial_batch_no_ledgers(ledgers, child_row)
+
+ return doc
+
+
+def create_serial_batch_no_ledgers(ledgers, child_row) -> object:
+ doc = frappe.get_doc(
+ {
+ "doctype": "Serial and Batch Bundle",
+ "voucher_type": child_row.parenttype,
+ "voucher_no": child_row.parent,
+ "item_code": child_row.item_code,
+ "voucher_detail_no": child_row.name,
+ }
+ )
+
+ for row in ledgers:
+ row = frappe._dict(row)
+ doc.append(
+ "ledgers",
+ {
+ "qty": row.qty or 1.0,
+ "warehouse": child_row.warehouse,
+ "batch_no": row.batch_no,
+ "serial_no": row.serial_no,
+ },
+ )
+
+ doc.save()
+
+ frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
+
+ frappe.msgprint(_("Serial and Batch Bundle created"), alert=True)
+
+ return doc
+
+
+def update_serial_batch_no_ledgers(ledgers, child_row) -> object:
+ doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
+ doc.voucher_detail_no = child_row.name
+ doc.set("ledgers", [])
+ doc.set("ledgers", ledgers)
+ doc.save()
+
+ frappe.msgprint(_("Serial and Batch Bundle updated"), alert=True)
+
+ return doc
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
new file mode 100644
index 0000000..02e5349
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestSerialandBatchBundle(FrappeTestCase):
+ pass
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py b/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
new file mode 100644
index 0000000..7fa9574
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
@@ -0,0 +1,73 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:55:15.909881",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "serial_no",
+ "batch_no",
+ "column_break_2",
+ "qty",
+ "warehouse",
+ "is_rejected"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:parent.has_serial_no == 1",
+ "fieldname": "serial_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Serial No",
+ "mandatory_depends_on": "eval:parent.has_serial_no == 1",
+ "options": "Serial No"
+ },
+ {
+ "depends_on": "eval:parent.has_batch_no == 1",
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Batch No",
+ "mandatory_depends_on": "eval:parent.has_batch_no == 1",
+ "options": "Batch"
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:parent.voucher_type == 'Purchase Receipt'",
+ "fieldname": "is_rejected",
+ "fieldtype": "Check",
+ "label": "Is Rejected"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-11-24 13:00:23.598351",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Serial and Batch Ledger",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py
new file mode 100644
index 0000000..945fdc1
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SerialandBatchLedger(Document):
+ pass
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 541d4d1..9338dc5 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -189,6 +189,7 @@
def get_last_sle(self, serial_no=None):
entries = {}
sle_dict = self.get_stock_ledger_entries(serial_no)
+ print("sle_dict", sle_dict)
if sle_dict:
if sle_dict.get("incoming", []):
entries["purchase_sle"] = sle_dict["incoming"][0]
@@ -206,33 +207,23 @@
if not serial_no:
serial_no = self.name
+ print("serial_no", serial_no)
for sle in frappe.db.sql(
"""
- SELECT voucher_type, voucher_no,
- posting_date, posting_time, incoming_rate, actual_qty, serial_no
+ SELECT sle.voucher_type, sle.voucher_no, serial_and_batch_bundle,
+ sle.posting_date, sle.posting_time, sle.incoming_rate, sle.actual_qty, snb.serial_no
FROM
- `tabStock Ledger Entry`
+ `tabStock Ledger Entry` sle, `tabSerial and Batch Ledger` snb
WHERE
- item_code=%s AND company = %s
- AND is_cancelled = 0
- AND (serial_no = %s
- OR serial_no like %s
- OR serial_no like %s
- OR serial_no like %s
- )
+ sle.item_code=%s AND sle.company = %s
+ AND sle.is_cancelled = 0
+ AND snb.serial_no = %s and snb.parent = sle.serial_and_batch_bundle
ORDER BY
- posting_date desc, posting_time desc, creation desc""",
- (
- self.item_code,
- self.company,
- serial_no,
- serial_no + "\n%",
- "%\n" + serial_no,
- "%\n" + serial_no + "\n%",
- ),
+ sle.posting_date desc, sle.posting_time desc, sle.creation desc""",
+ (self.item_code, self.company, serial_no),
as_dict=1,
):
- if serial_no.upper() in get_serial_nos(sle.serial_no):
+ if serial_no.upper() in get_serial_nos(sle.serial_and_batch_bundle):
if cint(sle.actual_qty) > 0:
sle_dict.setdefault("incoming", []).append(sle)
else:
@@ -262,6 +253,7 @@
def update_serial_no_reference(self, serial_no=None):
last_sle = self.get_last_sle(serial_no)
+ print(last_sle)
self.set_purchase_details(last_sle.get("purchase_sle"))
self.set_sales_details(last_sle.get("delivery_sle"))
self.set_maintenance_status()
@@ -275,7 +267,7 @@
def validate_serial_no(sle, item_det):
- serial_nos = get_serial_nos(sle.serial_no) if sle.serial_no else []
+ serial_nos = get_serial_nos(sle.serial_and_batch_bundle) if sle.serial_and_batch_bundle else []
validate_material_transfer_entry(sle)
if item_det.has_serial_no == 0:
@@ -541,7 +533,7 @@
return
if (
not sle.is_cancelled
- and not sle.serial_no
+ and not sle.serial_and_batch_bundle
and cint(sle.actual_qty) > 0
and item_det.has_serial_no == 1
and item_det.serial_no_series
@@ -549,7 +541,7 @@
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
sle.db_set("serial_no", serial_nos)
validate_serial_no(sle, item_det)
- if sle.serial_no:
+ if sle.serial_and_batch_bundle:
auto_make_serial_nos(sle)
@@ -569,7 +561,7 @@
def auto_make_serial_nos(args):
- serial_nos = get_serial_nos(args.get("serial_no"))
+ serial_nos = get_serial_nos(args.get("serial_and_batch_bundle"))
created_numbers = []
voucher_type = args.get("voucher_type")
item_code = args.get("item_code")
@@ -624,13 +616,14 @@
)[0]
-def get_serial_nos(serial_no):
- if isinstance(serial_no, list):
- return serial_no
+def get_serial_nos(serial_and_batch_bundle):
+ serial_nos = frappe.get_all(
+ "Serial and Batch Ledger",
+ filters={"parent": serial_and_batch_bundle, "serial_no": ("is", "set")},
+ fields=["serial_no"],
+ )
- return [
- s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
- ]
+ return [d.serial_no for d in serial_nos]
def clean_serial_no_string(serial_no: str) -> str:
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 46ce9de..0df0a04 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -31,6 +31,7 @@
"company",
"stock_uom",
"project",
+ "serial_and_batch_bundle",
"batch_no",
"column_break_26",
"fiscal_year",
@@ -309,6 +310,13 @@
"label": "Recalculate Incoming/Outgoing Rate",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": "Serial and Batch Bundle",
+ "options": "Serial and Batch Bundle",
+ "search_index": 1
}
],
"hide_toolbar": 1,
@@ -317,7 +325,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-12-21 06:25:30.040801",
+ "modified": "2022-11-24 13:14:31.974743",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 052f778..916b14a 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -40,7 +40,7 @@
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
self.validate_mandatory()
- self.validate_item()
+ self.validate_serial_batch_no_bundle()
self.validate_batch()
validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company)
@@ -79,47 +79,43 @@
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory"))
- def validate_item(self):
- item_det = frappe.db.sql(
- """select name, item_name, has_batch_no, docstatus,
- is_stock_item, has_variants, stock_uom, create_new_batch
- from tabItem where name=%s""",
+ def validate_serial_batch_no_bundle(self):
+ item_detail = frappe.get_cached_value(
+ "Item",
self.item_code,
- as_dict=True,
+ ["has_serial_no", "has_batch_no", "is_stock_item", "has_variants", "stock_uom"],
+ as_dict=1,
)
- if not item_det:
+ if not item_detail:
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))
-
- # check if batch number is valid
- if item_det.has_batch_no == 1:
- batch_item = (
- self.item_code
- if self.item_code == item_det.item_name
- else self.item_code + ":" + item_det.item_name
- )
- if not self.batch_no:
- frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
- 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, batch_item)
- )
-
- elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
- frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
-
- if item_det.has_variants:
+ if item_detail.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
+ if item_detail.is_stock_item != 1:
+ frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))
+
+ if item_detail.has_serial_no or item_detail.has_batch_no:
+ if not self.serial_and_batch_bundle:
+ frappe.throw(_(f"Serial No and Batch No are mandatory for Item {self.item_code}"))
+ elif self.item_code != frappe.get_cached_value(
+ "Serial and Batch Bundle", self.serial_and_batch_bundle, "item_code"
+ ):
+ frappe.throw(
+ _(
+ f"Serial No and Batch No Bundle {self.serial_and_batch_bundle} is not for Item {self.item_code}"
+ )
+ )
+
+ if self.serial_and_batch_bundle and not (item_detail.has_serial_no or item_detail.has_batch_no):
+ frappe.throw(_(f"Serial No and Batch No are not allowed for Item {self.item_code}"))
+
+ if self.stock_uom != item_detail.stock_uom:
+ self.stock_uom = item_detail.stock_uom
def check_stock_frozen_date(self):
stock_settings = frappe.get_cached_doc("Stock Settings")