refactor: serial no normalization

This commit is contained in:
Rohit Waghchaure 2022-10-10 13:28:19 +05:30
parent f11d9b019d
commit bc75a7ef44
23 changed files with 980 additions and 133 deletions

View File

@ -7,7 +7,7 @@ from typing import List, Tuple
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 @@ class StockController(AccountsController):
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 @@ class StockController(AccountsController):
)
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 @@ class StockController(AccountsController):
),
"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,

View File

@ -341,10 +341,36 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
}
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');

View File

@ -119,9 +119,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
});
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
}
}
});
}

View File

@ -616,3 +616,195 @@ function check_can_calculate_pending_qty(me) {
}
//# 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();
}
}

View File

@ -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) {
// }
});

View File

@ -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": []
}

View File

@ -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

View File

@ -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

View File

@ -7,6 +7,8 @@ frappe.provide("erpnext.stock");
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');

View File

@ -283,7 +283,12 @@ class PurchaseReceipt(BuyingController):
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()

View File

@ -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",

View File

@ -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
);
}
});

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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": []
}

View File

@ -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

View File

@ -189,6 +189,7 @@ class SerialNo(StockController):
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 @@ class SerialNo(StockController):
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 @@ class SerialNo(StockController):
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 process_serial_no(sle):
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 @@ def update_serial_nos(sle, item_det):
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 @@ def update_serial_nos(sle, item_det):
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 get_new_serial_number(series):
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 @@ def get_item_details(item_code):
)[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:

View File

@ -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",

View File

@ -40,7 +40,7 @@ class StockLedgerEntry(Document):
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 @@ class StockLedgerEntry(Document):
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")