fix: removed sales and purchase fields from serial nos
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 868a150..8ed11a4 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1448,6 +1448,7 @@
"Repost Payment Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
+ "Serial and Batch Bundle",
)
self.update_advance_tax_references(cancel=1)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 7454332..714f24a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -400,6 +400,7 @@
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry",
+ "Serial and Batch Bundle",
)
def update_status_updater_args(self):
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 7687aad..bd4bc18 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _, bold, throw
from frappe.contacts.doctype.address.address import get_address_display
-from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
+from frappe.utils import cint, flt, get_link_to_form, nowtime
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
@@ -299,8 +299,7 @@
"item_code": p.item_code,
"qty": flt(p.qty),
"uom": p.uom,
- "batch_no": cstr(p.batch_no).strip(),
- "serial_no": cstr(p.serial_no).strip(),
+ "serial_and_batch_bundle": p.serial_and_batch_bundle,
"name": d.name,
"target_warehouse": p.target_warehouse,
"company": self.company,
@@ -323,8 +322,7 @@
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
- "batch_no": cstr(d.get("batch_no")).strip(),
- "serial_no": cstr(d.get("serial_no")).strip(),
+ "serial_and_batch_bundle": d.serial_and_batch_bundle,
"name": d.name,
"target_warehouse": d.target_warehouse,
"company": self.company,
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 6156aba..6e71004 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -354,6 +354,7 @@
"batch_no": batch_no,
"qty": d.qty,
"warehouse": d.get(warehouse_field),
+ "incoming_rate": d.rate,
}
],
}
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 7584522..e30a302 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -33,7 +33,6 @@
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import (
- auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
@@ -455,7 +454,7 @@
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
- auto_make_serial_nos(args)
+ # auto_make_serial_nos(args)
serial_nos_length = len(get_serial_nos(self.serial_no))
if serial_nos_length != self.qty:
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index b4676c1..52abbc0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -6,6 +6,9 @@
setup() {
super.setup();
let me = this;
+
+ this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
+
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
@@ -124,7 +127,9 @@
let item_row = locals[cdt][cdn];
return {
filters: {
- 'item_code': item_row.item_code
+ 'item_code': item_row.item_code,
+ 'voucher_type': doc.doctype,
+ 'voucher_no': ["in", [doc.name, ""]],
}
}
});
@@ -2277,12 +2282,12 @@
}
};
-erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_dialog) {
+erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {
let warehouse, receiving_stock, existing_stock;
if (frm.doc.is_return) {
if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
existing_stock = true;
- warehouse = d.warehouse;
+ warehouse = item_row.warehouse;
} else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
receiving_stock = true;
}
@@ -2292,11 +2297,11 @@
receiving_stock = true;
} else {
existing_stock = true;
- warehouse = d.s_warehouse;
+ warehouse = item_row.s_warehouse;
}
} else {
existing_stock = true;
- warehouse = d.warehouse;
+ warehouse = item_row.warehouse;
}
}
@@ -2309,16 +2314,13 @@
}
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
- new erpnext.SerialNoBatchSelector({
- frm: frm,
- item: d,
- warehouse_details: {
- type: "Warehouse",
- name: warehouse
- },
- callback: callback,
- on_close: on_close
- }, show_dialog);
+ new erpnext.SerialNoBatchBundleUpdate(frm, item_row, (r) => {
+ if (r) {
+ frm.refresh_fields();
+ frappe.model.set_value(item_row.doctype, item_row.name,
+ "serial_and_batch_bundle", r.name);
+ }
+ });
});
}
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 90967d9..fcaaaf0 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -629,20 +629,37 @@
}
make() {
+ let label = this.item?.has_serial_no ? __('Serial No') : __('Batch No');
+ let primary_label = this.item?.serial_and_batch_bundle
+ ? __('Update') : __('Add');
+
+ if (this.item?.has_serial_no && this.item?.batch_no) {
+ label = __('Serial No / Batch No');
+ }
+
+ primary_label += ' ' + label;
+
this.dialog = new frappe.ui.Dialog({
- title: __('Update Serial No / Batch No'),
+ title: this.item?.title || primary_label,
fields: this.get_dialog_fields(),
- primary_action_label: __('Update'),
+ primary_action_label: primary_label,
primary_action: () => this.update_ledgers()
});
+
+ if (this.item?.outward) {
+ this.prepare_for_auto_fetch();
+ }
+
this.dialog.show();
}
get_serial_no_filters() {
+ let warehouse = this.item?.outward ?
+ this.item.warehouse : "";
+
return {
'item_code': this.item.item_code,
- 'warehouse': ["=", ""],
- 'delivery_document_no': ["=", ""],
+ 'warehouse': ["=", warehouse]
};
}
@@ -681,13 +698,15 @@
});
}
- if (this.item.has_batch_no && this.item.has_serial_no) {
- fields.push({
- fieldtype: 'Section Break',
- });
+ if (this.item?.outward) {
+ fields = [...fields, ...this.get_filter_fields()];
}
fields.push({
+ fieldtype: 'Section Break',
+ });
+
+ fields.push({
fieldname: 'ledgers',
fieldtype: 'Table',
allow_bulk_edit: true,
@@ -698,6 +717,41 @@
return fields;
}
+ get_filter_fields() {
+ return [
+ {
+ fieldtype: 'Section Break',
+ label: __('Auto Fetch')
+ },
+ {
+ fieldtype: 'Float',
+ fieldname: 'qty',
+ default: this.item.qty || 0,
+ label: __('Qty to Fetch'),
+ },
+ {
+ fieldtype: 'Column Break',
+ },
+ {
+ fieldtype: 'Select',
+ options: ['FIFO', 'LIFO', 'Expiry'],
+ default: 'FIFO',
+ fieldname: 'based_on',
+ label: __('Fetch Based On')
+ },
+ {
+ fieldtype: 'Column Break',
+ },
+ {
+ fieldtype: 'Button',
+ fieldname: 'get_auto_data',
+ label: __('Fetch {0}',
+ [this.item?.has_serial_no ? 'Serial Nos' : 'Batch Nos']),
+ },
+ ]
+
+ }
+
get_dialog_table_fields() {
let fields = []
@@ -714,7 +768,9 @@
}
}
})
- } else if (this.item.has_batch_no) {
+ }
+
+ if (this.item.has_batch_no) {
fields = [
{
fieldtype: 'Link',
@@ -742,6 +798,38 @@
return fields;
}
+ prepare_for_auto_fetch() {
+ this.dialog.fields_dict.get_auto_data.$input.on('click', () => {
+ this.get_auto_data();
+ });
+ }
+
+ get_auto_data() {
+ const { qty, based_on } = this.dialog.get_values();
+
+ if (!qty) {
+ frappe.throw(__('Please enter Qty to Fetch'));
+ }
+
+ frappe.call({
+ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data',
+ args: {
+ item_code: this.item.item_code,
+ warehouse: this.item.warehouse,
+ has_serial_no: this.item.has_serial_no,
+ has_batch_no: this.item.has_batch_no,
+ qty: qty,
+ based_on: based_on
+ },
+ callback: (r) => {
+ if (r.message) {
+ this.dialog.fields_dict.ledgers.df.data = r.message;
+ this.dialog.fields_dict.ledgers.grid.refresh();
+ }
+ }
+ });
+ }
+
update_serial_batch_no() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index e3de49c..f5268d6 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -420,6 +420,40 @@
});
}
+ pick_serial_and_batch(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;
+ item.outward = true;
+
+ item.title = item.has_serial_no ?
+ __("Select Serial No") : __("Select Batch No");
+
+ if (item.has_serial_no && item.has_batch_no) {
+ item.title = __("Select Serial and Batch");
+ }
+
+ 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);
+ }
+ }
+ );
+ });
+ }
+ });
+ }
+
update_auto_repeat_reference(doc) {
if (doc.auto_repeat) {
frappe.call({
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 2ee372e..a647a17 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -12,7 +12,6 @@
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
-from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -142,11 +141,6 @@
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
-
- if self._action != "submit" and not self.is_return:
- set_batch_nos(self, "warehouse", throw=True)
- set_batch_nos(self, "warehouse", throw=True, child_table="packed_items")
-
self.update_current_stock()
if not self.installation_status:
@@ -274,7 +268,12 @@
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",
+ )
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 3853bd1..3f77869 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -77,8 +77,8 @@
"dn_detail",
"pick_list_item",
"section_break_40",
- "batch_no",
- "serial_no",
+ "pick_serial_and_batch",
+ "serial_and_batch_bundle",
"actual_batch_qty",
"actual_qty",
"installed_qty",
@@ -508,16 +508,6 @@
"fieldtype": "Section Break"
},
{
- "fieldname": "batch_no",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Batch No",
- "oldfieldname": "batch_no",
- "oldfieldtype": "Link",
- "options": "Batch",
- "print_hide": 1
- },
- {
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
@@ -543,15 +533,6 @@
"width": "150px"
},
{
- "fieldname": "serial_no",
- "fieldtype": "Text",
- "in_list_view": 1,
- "label": "Serial No",
- "no_copy": 1,
- "oldfieldname": "serial_no",
- "oldfieldtype": "Text"
- },
- {
"fieldname": "item_group",
"fieldtype": "Link",
"hidden": 1,
@@ -861,6 +842,17 @@
"no_copy": 1,
"non_negative": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": "Serial and Batch Bundle",
+ "options": "Serial and Batch Bundle"
+ },
+ {
+ "fieldname": "pick_serial_and_batch",
+ "fieldtype": "Button",
+ "label": "Pick Serial / Batch No"
}
],
"idx": 1,
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index c5fb241..244c905 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -19,6 +19,7 @@
"rate",
"uom",
"section_break_9",
+ "serial_and_batch_bundle",
"serial_no",
"column_break_11",
"batch_no",
@@ -253,6 +254,12 @@
"no_copy": 1,
"non_negative": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": "Serial and Batch Bundle",
+ "options": "Serial and Batch Bundle"
}
],
"idx": 1,
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index e0cb8ca..312c166 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -7,8 +7,6 @@
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/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
index 085e33d..f16a72b 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
@@ -10,6 +10,36 @@
frm.trigger('toggle_fields');
},
+ warehouse(frm) {
+ if (frm.doc.warehouse) {
+ frm.call({
+ method: "set_warehouse",
+ doc: frm.doc,
+ callback(r) {
+ refresh_field("ledgers");
+ }
+ })
+ }
+ },
+
+ 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
+ );
+ },
+
set_queries(frm) {
frm.set_query('item_code', () => {
return {
@@ -35,6 +65,15 @@
};
});
+ frm.set_query('warehouse', () => {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': frm.doc.company,
+ }
+ };
+ });
+
frm.set_query('serial_no', 'ledgers', () => {
return {
filters: {
@@ -58,23 +97,14 @@
}
};
});
- },
-
- 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
- );
}
});
+
+
+frappe.ui.form.on("Serial and Batch Ledger", {
+ ledgers_add(frm, cdt, cdn) {
+ if (frm.doc.warehouse) {
+ locals[cdt][cdn].warehouse = frm.doc.warehouse;
+ }
+ },
+})
\ No newline at end of file
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 a08ed83..cfe35d7 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
@@ -8,17 +8,23 @@
"item_details_tab",
"company",
"item_group",
- "has_serial_no",
+ "warehouse",
"column_break_4",
"item_code",
"item_name",
+ "has_serial_no",
"has_batch_no",
"serial_no_and_batch_no_tab",
"ledgers",
- "qty",
+ "quantity_and_rate_section",
+ "total_qty",
+ "column_break_13",
+ "avg_rate",
+ "total_amount",
"tab_break_12",
"voucher_type",
"voucher_no",
+ "column_break_aouy",
"is_cancelled",
"amended_from"
],
@@ -91,12 +97,6 @@
"reqd": 1
},
{
- "fieldname": "qty",
- "fieldtype": "Float",
- "label": "Total Qty",
- "read_only": 1
- },
- {
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
@@ -129,12 +129,54 @@
"fieldname": "tab_break_12",
"fieldtype": "Tab Break",
"label": "Reference"
+ },
+ {
+ "fieldname": "quantity_and_rate_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity and Rate"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "avg_rate",
+ "fieldtype": "Float",
+ "label": "Avg Rate",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_amount",
+ "fieldtype": "Float",
+ "label": "Total Amount",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_qty",
+ "fieldtype": "Float",
+ "label": "Total Qty",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_aouy",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "company",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-24 13:05:11.623968",
+ "modified": "2023-01-10 11:32:09.018760",
"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 554c032..1c9dc15 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
@@ -1,20 +1,114 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import collections
+
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.functions import Sum
+from frappe.utils import cint, flt, today
+from pypika import Case
class SerialandBatchBundle(Document):
def validate(self):
self.validate_serial_and_batch_no()
+ self.validate_duplicate_serial_and_batch_no()
+
+ def before_save(self):
+ self.set_outgoing_rate()
+
+ if self.ledgers:
+ self.set_total_qty()
+ self.set_avg_rate()
+
+ @frappe.whitelist()
+ def set_warehouse(self):
+ for row in self.ledgers:
+ row.warehouse = self.warehouse
+
+ def set_total_qty(self):
+ self.total_qty = sum([row.qty for row in self.ledgers])
+
+ def set_avg_rate(self):
+ self.total_amount = 0.0
+
+ for row in self.ledgers:
+ rate = flt(row.incoming_rate) or flt(row.outgoing_rate)
+ self.total_amount += flt(row.qty) * rate
+
+ if self.total_qty:
+ self.avg_rate = flt(self.total_amount) / flt(self.total_qty)
+
+ def set_outgoing_rate(self, update_rate=False):
+ if not self.calculate_outgoing_rate():
+ return
+
+ serial_nos = [row.serial_no for row in self.ledgers]
+ data = get_serial_and_batch_ledger(
+ item_code=self.item_code,
+ warehouse=self.ledgers[0].warehouse,
+ serial_nos=serial_nos,
+ fetch_incoming_rate=True,
+ )
+
+ if not data:
+ return
+
+ serial_no_details = {row.serial_no: row for row in data}
+
+ for ledger in self.ledgers:
+ if sn_details := serial_no_details.get(ledger.serial_no):
+ if ledger.outgoing_rate and ledger.outgoing_rate == sn_details.incoming_rate:
+ continue
+
+ ledger.outgoing_rate = sn_details.incoming_rate or 0.0
+ if update_rate:
+ ledger.db_set("outgoing_rate", ledger.outgoing_rate)
+
+ def calculate_outgoing_rate(self):
+ if not (self.has_serial_no and self.ledgers):
+ return
+
+ if not (self.voucher_type and self.voucher_no):
+ return False
+
+ if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
+ return frappe.get_cached_value(self.voucher_type, self.voucher_no, "is_return")
+ elif self.voucher_type in ["Sales Invoice", "Delivery Note"]:
+ return not frappe.get_cached_value(self.voucher_type, self.voucher_no, "is_return")
+ elif self.voucher_type == "Stock Entry":
+ return frappe.get_cached_value(self.voucher_type, self.voucher_no, "purpose") in [
+ "Material Receipt"
+ ]
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 validate_duplicate_serial_and_batch_no(self):
+ serial_nos = []
+ batch_nos = []
+
+ for row in self.ledgers:
+ if row.serial_no:
+ serial_nos.append(row.serial_no)
+
+ if row.batch_no:
+ batch_nos.append(row.batch_no)
+
+ if serial_nos:
+ for key, value in collections.Counter(serial_nos).items():
+ if value > 1:
+ frappe.throw(_(f"Duplicate Serial No {key} found"))
+
+ if batch_nos:
+ for key, value in collections.Counter(batch_nos).items():
+ if value > 1:
+ frappe.throw(_(f"Duplicate Batch No {key} found"))
+
def before_cancel(self):
self.delink_serial_and_batch_bundle()
self.clear_table()
@@ -30,6 +124,35 @@
def clear_table(self):
self.set("ledgers", [])
+ def delink_refernce_from_voucher(self):
+ child_table = f"{self.voucher_type} Item"
+ if self.voucher_type == "Stock Entry":
+ child_table = f"{self.voucher_type} Detail"
+
+ vouchers = frappe.get_all(
+ child_table,
+ fields=["name"],
+ filters={"serial_and_batch_bundle": self.name, "docstatus": 0},
+ )
+
+ for voucher in vouchers:
+ frappe.db.set_value(child_table, voucher.name, "serial_and_batch_bundle", None)
+
+ def delink_reference_from_batch(self):
+ batches = frappe.get_all(
+ "Batch",
+ fields=["name"],
+ filters={"reference_name": self.name, "reference_doctype": "Serial and Batch Bundle"},
+ )
+
+ for batch in batches:
+ frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
+
+ def on_trash(self):
+ self.delink_refernce_from_voucher()
+ self.delink_reference_from_batch()
+ self.clear_table()
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -125,3 +248,144 @@
frappe.msgprint(_("Serial and Batch Bundle updated"), alert=True)
return doc
+
+
+def get_serial_and_batch_ledger(**kwargs):
+ kwargs = frappe._dict(kwargs)
+
+ sle_table = frappe.qb.DocType("Stock Ledger Entry")
+ serial_batch_table = frappe.qb.DocType("Serial and Batch Ledger")
+
+ query = (
+ frappe.qb.from_(sle_table)
+ .inner_join(serial_batch_table)
+ .on(sle_table.serial_and_batch_bundle == serial_batch_table.parent)
+ .select(
+ serial_batch_table.serial_no,
+ serial_batch_table.warehouse,
+ serial_batch_table.batch_no,
+ serial_batch_table.qty,
+ serial_batch_table.incoming_rate,
+ )
+ .where((sle_table.item_code == kwargs.item_code) & (sle_table.warehouse == kwargs.warehouse))
+ )
+
+ if kwargs.serial_nos:
+ query = query.where(serial_batch_table.serial_no.isin(kwargs.serial_nos))
+
+ if kwargs.batch_nos:
+ query = query.where(serial_batch_table.batch_no.isin(kwargs.batch_nos))
+
+ if kwargs.fetch_incoming_rate:
+ query = query.where(sle_table.actual_qty > 0)
+
+ return query.run(as_dict=True)
+
+
+def get_copy_of_serial_and_batch_bundle(serial_and_batch_bundle, warehouse):
+ bundle_doc = frappe.copy_doc(serial_and_batch_bundle)
+ for row in bundle_doc.ledgers:
+ row.warehouse = warehouse
+ row.incoming_rate = row.outgoing_rate
+ row.outgoing_rate = 0.0
+
+ return bundle_doc.submit(ignore_permissions=True)
+
+
+@frappe.whitelist()
+def get_auto_data(**kwargs):
+ kwargs = frappe._dict(kwargs)
+
+ if cint(kwargs.has_serial_no):
+ return get_auto_serial_nos(kwargs)
+
+ elif cint(kwargs.has_batch_no):
+ return get_auto_batch_nos(kwargs)
+
+
+def get_auto_serial_nos(kwargs):
+ fields = ["name as serial_no"]
+ if kwargs.has_batch_no:
+ fields.append("batch_no")
+
+ order_by = "creation"
+ if kwargs.based_on == "LIFO":
+ order_by = "creation desc"
+ elif kwargs.based_on == "Expiry":
+ order_by = "amc_expiry_date asc"
+
+ return frappe.get_all(
+ "Serial No",
+ fields=fields,
+ filters={"item_code": kwargs.item_code, "warehouse": kwargs.warehouse},
+ limit=cint(kwargs.qty),
+ order_by=order_by,
+ )
+
+
+def get_auto_batch_nos(kwargs):
+ available_batches = get_available_batches(kwargs)
+
+ qty = flt(kwargs.qty)
+
+ batches = []
+
+ for batch in available_batches:
+ if qty > 0:
+ batch_qty = flt(batch.qty)
+ if qty > batch_qty:
+ batches.append(
+ {
+ "batch_no": batch.batch_no,
+ "qty": batch_qty,
+ }
+ )
+ qty -= batch_qty
+ else:
+ batches.append(
+ {
+ "batch_no": batch.batch_no,
+ "qty": qty,
+ }
+ )
+ qty = 0
+
+ return batches
+
+
+def get_available_batches(kwargs):
+ stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
+ batch_ledger = frappe.qb.DocType("Serial and Batch Ledger")
+ batch_table = frappe.qb.DocType("Batch")
+
+ query = (
+ frappe.qb.from_(stock_ledger_entry)
+ .inner_join(batch_ledger)
+ .on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent)
+ .inner_join(batch_table)
+ .on(batch_ledger.batch_no == batch_table.name)
+ .select(
+ batch_ledger.batch_no,
+ Sum(
+ Case().when(stock_ledger_entry.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)
+ ).as_("qty"),
+ )
+ .where(
+ (stock_ledger_entry.item_code == kwargs.item_code)
+ & (stock_ledger_entry.warehouse == kwargs.warehouse)
+ & ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
+ )
+ .groupby(batch_ledger.batch_no)
+ )
+
+ if kwargs.based_on == "LIFO":
+ query = query.orderby(batch_table.creation, order=frappe.qb.desc)
+ elif kwargs.based_on == "Expiry":
+ query = query.orderby(batch_table.expiry_date)
+ else:
+ query = query.orderby(batch_table.creation)
+
+ data = query.run(as_dict=True)
+ data = list(filter(lambda x: x.qty > 0, data))
+
+ return data
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
index 7fa9574..65eaa03 100644
--- 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
@@ -5,12 +5,17 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "item_code",
"serial_no",
"batch_no",
"column_break_2",
"qty",
"warehouse",
- "is_rejected"
+ "section_break_6",
+ "incoming_rate",
+ "column_break_8",
+ "outgoing_rate",
+ "stock_value_difference"
],
"fields": [
{
@@ -34,6 +39,7 @@
"options": "Batch"
},
{
+ "default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
@@ -47,21 +53,52 @@
"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"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break",
+ "label": "Rate Section"
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Float",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "outgoing_rate",
+ "fieldtype": "Float",
+ "label": "Outgoing Rate",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "stock_value_difference",
+ "fieldtype": "Float",
+ "label": "Change in Stock Value",
+ "no_copy": 1,
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-24 13:00:23.598351",
+ "modified": "2023-01-10 12:55:57.368650",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Ledger",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index 7989b1a..7f22af1 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -12,24 +12,13 @@
"column_break0",
"serial_no",
"item_code",
- "warehouse",
"batch_no",
+ "warehouse",
"column_break1",
"item_name",
"description",
"item_group",
"brand",
- "sales_order",
- "purchase_details",
- "column_break2",
- "purchase_document_type",
- "purchase_document_no",
- "purchase_date",
- "purchase_time",
- "purchase_rate",
- "column_break3",
- "supplier",
- "supplier_name",
"asset_details",
"asset",
"asset_status",
@@ -38,14 +27,6 @@
"employee",
"delivery_details",
"delivery_document_type",
- "delivery_document_no",
- "delivery_date",
- "delivery_time",
- "column_break5",
- "customer",
- "customer_name",
- "invoice_details",
- "sales_invoice",
"warranty_amc_details",
"column_break6",
"warranty_expiry_date",
@@ -56,7 +37,6 @@
"more_info",
"serial_no_details",
"company",
- "status",
"work_order"
],
"fields": [
@@ -91,29 +71,6 @@
"reqd": 1
},
{
- "description": "Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt",
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Warehouse",
- "no_copy": 1,
- "oldfieldname": "warehouse",
- "oldfieldtype": "Link",
- "options": "Warehouse",
- "read_only": 1,
- "search_index": 1
- },
- {
- "fieldname": "batch_no",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Batch No",
- "options": "Batch",
- "read_only": 1
- },
- {
"fieldname": "column_break1",
"fieldtype": "Column Break"
},
@@ -151,84 +108,6 @@
"read_only": 1
},
{
- "fieldname": "sales_order",
- "fieldtype": "Link",
- "label": "Sales Order",
- "options": "Sales Order"
- },
- {
- "fieldname": "purchase_details",
- "fieldtype": "Section Break",
- "label": "Purchase / Manufacture Details"
- },
- {
- "fieldname": "column_break2",
- "fieldtype": "Column Break",
- "width": "50%"
- },
- {
- "fieldname": "purchase_document_type",
- "fieldtype": "Link",
- "label": "Creation Document Type",
- "no_copy": 1,
- "options": "DocType",
- "read_only": 1
- },
- {
- "fieldname": "purchase_document_no",
- "fieldtype": "Dynamic Link",
- "label": "Creation Document No",
- "no_copy": 1,
- "options": "purchase_document_type",
- "read_only": 1
- },
- {
- "fieldname": "purchase_date",
- "fieldtype": "Date",
- "label": "Creation Date",
- "no_copy": 1,
- "oldfieldname": "purchase_date",
- "oldfieldtype": "Date",
- "read_only": 1
- },
- {
- "fieldname": "purchase_time",
- "fieldtype": "Time",
- "label": "Creation Time",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "purchase_rate",
- "fieldtype": "Currency",
- "label": "Incoming Rate",
- "no_copy": 1,
- "oldfieldname": "purchase_rate",
- "oldfieldtype": "Currency",
- "options": "Company:company:default_currency",
- "read_only": 1
- },
- {
- "fieldname": "column_break3",
- "fieldtype": "Column Break",
- "width": "50%"
- },
- {
- "fieldname": "supplier",
- "fieldtype": "Link",
- "label": "Supplier",
- "no_copy": 1,
- "options": "Supplier"
- },
- {
- "bold": 1,
- "fieldname": "supplier_name",
- "fieldtype": "Data",
- "label": "Supplier Name",
- "no_copy": 1,
- "read_only": 1
- },
- {
"fieldname": "asset_details",
"fieldtype": "Section Break",
"label": "Asset Details"
@@ -284,67 +163,6 @@
"read_only": 1
},
{
- "fieldname": "delivery_document_no",
- "fieldtype": "Dynamic Link",
- "label": "Delivery Document No",
- "no_copy": 1,
- "options": "delivery_document_type",
- "read_only": 1
- },
- {
- "fieldname": "delivery_date",
- "fieldtype": "Date",
- "label": "Delivery Date",
- "no_copy": 1,
- "oldfieldname": "delivery_date",
- "oldfieldtype": "Date",
- "read_only": 1
- },
- {
- "fieldname": "delivery_time",
- "fieldtype": "Time",
- "label": "Delivery Time",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "column_break5",
- "fieldtype": "Column Break",
- "width": "50%"
- },
- {
- "fieldname": "customer",
- "fieldtype": "Link",
- "label": "Customer",
- "no_copy": 1,
- "oldfieldname": "customer",
- "oldfieldtype": "Link",
- "options": "Customer",
- "print_hide": 1
- },
- {
- "bold": 1,
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "label": "Customer Name",
- "no_copy": 1,
- "oldfieldname": "customer_name",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "fieldname": "invoice_details",
- "fieldtype": "Section Break",
- "label": "Invoice Details"
- },
- {
- "fieldname": "sales_invoice",
- "fieldtype": "Link",
- "label": "Sales Invoice",
- "options": "Sales Invoice",
- "read_only": 1
- },
- {
"fieldname": "warranty_amc_details",
"fieldtype": "Section Break",
"label": "Warranty / AMC Details"
@@ -408,6 +226,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@@ -416,24 +235,29 @@
"set_only_once": 1
},
{
- "fieldname": "status",
- "fieldtype": "Select",
- "in_standard_filter": 1,
- "label": "Status",
- "options": "\nActive\nInactive\nDelivered\nExpired",
- "read_only": 1
- },
- {
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ "read_only": 1
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch"
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2023-04-14 15:58:46.139887",
+ "modified": "2023-04-15 15:58:46.139887",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 98beda0..6d92cc3 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -9,17 +9,7 @@
from frappe import ValidationError, _
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Coalesce
-from frappe.utils import (
- add_days,
- cint,
- cstr,
- flt,
- get_link_to_form,
- getdate,
- now,
- nowdate,
- safe_json_loads,
-)
+from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.get_item_details import get_reserved_qty_for_so
@@ -80,19 +70,6 @@
)
self.set_maintenance_status()
- self.validate_warehouse()
- self.validate_item()
- self.set_status()
-
- def set_status(self):
- if self.delivery_document_type:
- self.status = "Delivered"
- elif self.warranty_expiry_date and getdate(self.warranty_expiry_date) <= getdate(nowdate()):
- self.status = "Expired"
- elif not self.warehouse:
- self.status = "Inactive"
- else:
- self.status = "Active"
def set_maintenance_status(self):
if not self.warranty_expiry_date and not self.amc_expiry_date:
@@ -110,127 +87,6 @@
if self.warranty_expiry_date and getdate(self.warranty_expiry_date) >= getdate(nowdate()):
self.maintenance_status = "Under Warranty"
- def validate_warehouse(self):
- if not self.get("__islocal"):
- item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"])
- if not self.via_stock_ledger and item_code != self.item_code:
- frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError)
- if not self.via_stock_ledger and warehouse != self.warehouse:
- frappe.throw(_("Warehouse cannot be changed for Serial No."), SerialNoCannotCannotChangeError)
-
- def validate_item(self):
- """
- Validate whether serial no is required for this item
- """
- item = frappe.get_cached_doc("Item", self.item_code)
- if item.has_serial_no != 1:
- frappe.throw(
- _("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)
- )
-
- self.item_group = item.item_group
- self.description = item.description
- self.item_name = item.item_name
- self.brand = item.brand
- self.warranty_period = item.warranty_period
-
- def set_purchase_details(self, purchase_sle):
- if purchase_sle:
- self.purchase_document_type = purchase_sle.voucher_type
- self.purchase_document_no = purchase_sle.voucher_no
- self.purchase_date = purchase_sle.posting_date
- self.purchase_time = purchase_sle.posting_time
- self.purchase_rate = purchase_sle.incoming_rate
- if purchase_sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
- self.supplier, self.supplier_name = frappe.db.get_value(
- purchase_sle.voucher_type, purchase_sle.voucher_no, ["supplier", "supplier_name"]
- )
-
- # If sales return entry
- if self.purchase_document_type == "Delivery Note":
- self.sales_invoice = None
- else:
- for fieldname in (
- "purchase_document_type",
- "purchase_document_no",
- "purchase_date",
- "purchase_time",
- "purchase_rate",
- "supplier",
- "supplier_name",
- ):
- self.set(fieldname, None)
-
- def set_sales_details(self, delivery_sle):
- if delivery_sle:
- self.delivery_document_type = delivery_sle.voucher_type
- self.delivery_document_no = delivery_sle.voucher_no
- self.delivery_date = delivery_sle.posting_date
- self.delivery_time = delivery_sle.posting_time
- if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"):
- self.customer, self.customer_name = frappe.db.get_value(
- delivery_sle.voucher_type, delivery_sle.voucher_no, ["customer", "customer_name"]
- )
- if self.warranty_period:
- self.warranty_expiry_date = add_days(
- cstr(delivery_sle.posting_date), cint(self.warranty_period)
- )
- else:
- for fieldname in (
- "delivery_document_type",
- "delivery_document_no",
- "delivery_date",
- "delivery_time",
- "customer",
- "customer_name",
- "warranty_expiry_date",
- ):
- self.set(fieldname, None)
-
- def get_last_sle(self, serial_no=None):
- entries = {}
- sle_dict = self.get_stock_ledger_entries(serial_no)
- if sle_dict:
- if sle_dict.get("incoming", []):
- entries["purchase_sle"] = sle_dict["incoming"][0]
-
- if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
- entries["last_sle"] = sle_dict["incoming"][0]
- else:
- entries["last_sle"] = sle_dict["outgoing"][0]
- entries["delivery_sle"] = sle_dict["outgoing"][0]
-
- return entries
-
- def get_stock_ledger_entries(self, serial_no=None):
- sle_dict = {}
- if not serial_no:
- serial_no = self.name
-
- print("serial_no", serial_no)
- for sle in frappe.db.sql(
- """
- 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` sle, `tabSerial and Batch Ledger` snb
- WHERE
- 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
- 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_and_batch_bundle):
- if cint(sle.actual_qty) > 0:
- sle_dict.setdefault("incoming", []).append(sle)
- else:
- sle_dict.setdefault("outgoing", []).append(sle)
-
- return sle_dict
-
def on_trash(self):
sl_entries = frappe.db.sql(
"""select serial_no from `tabStock Ledger Entry`
@@ -251,19 +107,11 @@
_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)
)
- 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()
- self.set_status()
-
def process_serial_no(sle):
item_det = get_item_details(sle.item_code)
validate_serial_no(sle, item_det)
- update_serial_nos(sle, item_det)
+ create_serial_nos(sle, item_det)
def validate_serial_no(sle, item_det):
@@ -277,6 +125,7 @@
SerialNoNotRequiredError,
)
elif not sle.is_cancelled:
+ return
if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(
@@ -440,6 +289,7 @@
_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError
)
elif serial_nos:
+ return
# SLE is being cancelled and has serial nos
for serial_no in serial_nos:
check_serial_no_validity_on_cancel(serial_no, sle)
@@ -528,7 +378,7 @@
return allow_serial_nos
-def update_serial_nos(sle, item_det):
+def create_serial_nos(sle, item_det):
if sle.skip_update_serial_no:
return
if (
@@ -538,7 +388,7 @@
and item_det.has_serial_no == 1
and item_det.serial_no_series
):
- bundle = make_serial_bundle(sle, item_det)
+ bundle = make_serial_no_bundle(sle, item_det)
if bundle:
sle.db_set("serial_and_batch_bundle", bundle.name)
child_doctype = sle.voucher_type + " Item"
@@ -552,64 +402,127 @@
)
elif sle.serial_and_batch_bundle:
- auto_make_serial_nos(sle)
-
-
-def make_serial_bundle(sle, item_details):
- sr_nos = auto_create_serial_nos(sle, item_details)
-
- if sr_nos:
- sn_doc = frappe.new_doc("Serial and Batch Bundle")
- sn_doc.item_code = item_details.name
- sn_doc.item_name = item_details.item_name
- sn_doc.item_group = item_details.item_group
- sn_doc.has_serial_no = item_details.has_serial_no
- sn_doc.has_batch_no = item_details.has_batch_no
- sn_doc.voucher_type = sle.voucher_type
- sn_doc.voucher_no = sle.voucher_no
- sn_doc.flags.ignore_mandatory = True
- sn_doc.qty = sle.actual_qty
- sn_doc.insert()
-
- batch_no = ""
- if item_details.has_batch_no:
- batch_no = create_batch_for_serial_no(sle)
-
- ledgers = []
- fields = [
- "name",
- "serial_no",
- "batch_no",
- "warehouse",
- "qty",
- "parent",
- "parenttype",
- "parentfield",
- ]
-
- for serial_no in sr_nos:
- ledgers.append(
- (
- frappe.generate_hash("", 10),
- serial_no,
- batch_no,
- sle.warehouse,
- 1,
- sn_doc.name,
- sn_doc.doctype,
- "ledgers",
- )
+ if sle.is_cancelled:
+ frappe.db.set_value(
+ "Serial and Batch Bundle",
+ sle.serial_and_batch_bundle,
+ "is_cancelled",
+ 1,
)
- frappe.db.bulk_insert(
- "Serial and Batch Ledger",
- fields=fields,
- values=set(ledgers),
- ignore_duplicates=True,
+ if item_det.has_serial_no:
+ update_warehouse_in_serial_no(sle, item_det)
+
+
+def update_warehouse_in_serial_no(sle, item_det):
+ serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
+ serial_no_data = get_serial_nos_warehouse(sle.item_code, serial_nos)
+
+ if not serial_no_data:
+ for serial_no in serial_nos:
+ frappe.db.set_value("Serial No", serial_no, "warehouse", None)
+
+ else:
+ for row in serial_no_data:
+ if not row.serial_no:
+ continue
+
+ warehouse = row.warehouse if row.actual_qty > 0 else None
+ frappe.db.set_value("Serial No", row.serial_no, "warehouse", warehouse)
+
+
+def get_serial_nos_warehouse(item_code, serial_nos):
+ ledger_table = frappe.qb.DocType("Serial and Batch Ledger")
+ sle_table = frappe.qb.DocType("Stock Ledger Entry")
+
+ return (
+ frappe.qb.from_(ledger_table)
+ .inner_join(sle_table)
+ .on(ledger_table.parent == sle_table.serial_and_batch_bundle)
+ .select(
+ ledger_table.serial_no,
+ sle_table.actual_qty,
+ ledger_table.warehouse,
+ )
+ .where(
+ (ledger_table.serial_no.isin(serial_nos))
+ & (sle_table.is_cancelled == 0)
+ & (sle_table.item_code == item_code)
+ & (sle_table.serial_and_batch_bundle.isnotnull())
+ )
+ .orderby(sle_table.posting_date, order=frappe.qb.desc)
+ .orderby(sle_table.posting_time, order=frappe.qb.desc)
+ .orderby(sle_table.creation, order=frappe.qb.desc)
+ .groupby(ledger_table.serial_no)
+ ).run(as_dict=True)
+
+
+def make_serial_no_bundle(sle, item_details):
+ sr_nos = auto_create_serial_nos(sle, item_details)
+ if sr_nos:
+ return make_serial_batch_bundle(sle, item_details, sr_nos)
+
+
+def make_serial_batch_bundle(sle, item_details, sr_nos):
+ sn_doc = frappe.new_doc("Serial and Batch Bundle")
+ sn_doc.item_code = item_details.name
+ sn_doc.item_name = item_details.item_name
+ sn_doc.item_group = item_details.item_group
+ sn_doc.has_serial_no = item_details.has_serial_no
+ sn_doc.has_batch_no = item_details.has_batch_no
+ sn_doc.voucher_type = sle.voucher_type
+ sn_doc.voucher_no = sle.voucher_no
+ sn_doc.flags.ignore_mandatory = True
+ sn_doc.flags.ignore_validate = True
+ sn_doc.total_qty = sle.actual_qty
+ sn_doc.avg_rate = sle.incoming_rate
+ sn_doc.total_amount = flt(sle.actual_qty) * flt(sle.incoming_rate)
+ sn_doc.insert()
+
+ batch_no = ""
+ if item_details.has_batch_no:
+ batch_no = create_batch_for_serial_no(sle)
+
+ add_serial_no_to_bundle(sn_doc, sle, sr_nos, batch_no, item_details)
+
+ sn_doc.load_from_db()
+ sn_doc.flags.ignore_validate = True
+ return sn_doc.submit()
+
+
+def add_serial_no_to_bundle(sn_doc, sle, sr_nos, batch_no, item_details):
+ ledgers = []
+
+ fields = [
+ "name",
+ "serial_no",
+ "batch_no",
+ "warehouse",
+ "item_code",
+ "qty",
+ "incoming_rate",
+ "parent",
+ "parenttype",
+ "parentfield",
+ ]
+
+ for serial_no in sr_nos:
+ ledgers.append(
+ (
+ frappe.generate_hash("Serial and Batch Ledger", 10),
+ serial_no,
+ batch_no,
+ sle.warehouse,
+ item_details.item_code,
+ 1,
+ sle.incoming_rate,
+ sn_doc.name,
+ sn_doc.doctype,
+ "ledgers",
+ )
)
- sn_doc.load_from_db()
- return sn_doc.submit()
+ frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers))
def create_batch_for_serial_no(sle):
@@ -629,6 +542,10 @@
def auto_create_serial_nos(sle, item_details) -> List[str]:
sr_nos = []
serial_nos_details = []
+ current_series = frappe.db.sql(
+ "select current from `tabSeries` where name = %s", item_details.serial_no_series
+ )
+
for i in range(cint(sle.actual_qty)):
serial_no = make_autoname(item_details.serial_no_series, "Serial No")
sr_nos.append(serial_no)
@@ -640,13 +557,8 @@
now(),
frappe.session.user,
frappe.session.user,
- sle.voucher_type,
- sle.voucher_no,
sle.warehouse,
sle.company,
- sle.posting_date,
- sle.posting_time,
- sle.incoming_rate,
sle.item_code,
item_details.item_name,
item_details.description,
@@ -661,24 +573,14 @@
"modified",
"owner",
"modified_by",
- "purchase_document_type",
- "purchase_document_no",
"warehouse",
"company",
- "purchase_date",
- "purchase_time",
- "purchase_rate",
"item_code",
"item_name",
"description",
]
- frappe.db.bulk_insert(
- "Serial No",
- fields=fields,
- values=set(serial_nos_details),
- ignore_duplicates=True,
- )
+ frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
return sr_nos
@@ -698,41 +600,6 @@
return sr_no
-def auto_make_serial_nos(args):
- 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")
- for serial_no in serial_nos:
- is_new = False
- if frappe.db.exists("Serial No", serial_no):
- sr = frappe.get_cached_doc("Serial No", serial_no)
- elif args.get("actual_qty", 0) > 0:
- sr = frappe.new_doc("Serial No")
- is_new = True
-
- sr = update_args_for_serial_no(sr, serial_no, args, is_new=is_new)
- if is_new:
- created_numbers.append(sr.name)
-
- form_links = list(map(lambda d: get_link_to_form("Serial No", d), created_numbers))
-
- # Setting up tranlated title field for all cases
- singular_title = _("Serial Number Created")
- multiple_title = _("Serial Numbers Created")
-
- if voucher_type:
- multiple_title = singular_title = _("{0} Created").format(voucher_type)
-
- if len(form_links) == 1:
- frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title)
- elif len(form_links) > 0:
- message = _("The following serial numbers were created: <br><br> {0}").format(
- get_items_html(form_links, item_code)
- )
- frappe.msgprint(message, multiple_title)
-
-
def get_items_html(serial_nos, item_code):
body = ", ".join(serial_nos)
return """<details><summary>
@@ -773,36 +640,8 @@
return "\n".join(serial_no_list)
-def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
- for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
- if args.get(field):
- serial_no_doc.set(field, args.get(field))
-
- serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
- serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None
-
- if is_new:
- serial_no_doc.serial_no = serial_no
-
- if (
- serial_no_doc.sales_order
- and args.get("voucher_type") == "Stock Entry"
- and not args.get("actual_qty", 0) > 0
- ):
- serial_no_doc.sales_order = None
-
- serial_no_doc.validate_item()
- serial_no_doc.update_serial_no_reference(serial_no)
-
- if is_new:
- serial_no_doc.db_insert()
- else:
- serial_no_doc.db_update()
-
- return serial_no_doc
-
-
def update_serial_nos_after_submit(controller, parentfield):
+ return
stock_ledger_entries = frappe.db.sql(
"""select voucher_detail_no, serial_no, actual_qty, warehouse
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
diff --git a/erpnext/stock/doctype/serial_no/serial_no_list.js b/erpnext/stock/doctype/serial_no/serial_no_list.js
deleted file mode 100644
index 7526d1d..0000000
--- a/erpnext/stock/doctype/serial_no/serial_no_list.js
+++ /dev/null
@@ -1,14 +0,0 @@
-frappe.listview_settings['Serial No'] = {
- add_fields: ["item_code", "warehouse", "warranty_expiry_date", "delivery_document_type"],
- get_indicator: (doc) => {
- if (doc.delivery_document_type) {
- return [__("Delivered"), "green", "delivery_document_type,is,set"];
- } else if (doc.warranty_expiry_date && frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0) {
- return [__("Expired"), "red", "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set"];
- } else if (!doc.warehouse) {
- return [__("Inactive"), "grey", "warehouse,is,not set"];
- } else {
- return [__("Active"), "green", "delivery_document_type,is,not set"];
- }
- }
-};
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 55b950b..3263ed4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -29,6 +29,9 @@
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos
from erpnext.stock.doctype.item.item import get_item_defaults
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_copy_of_serial_and_batch_bundle,
+)
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos,
update_serial_nos_after_submit,
@@ -232,7 +235,12 @@
self.update_work_order()
self.update_stock_ledger()
- 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.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
@@ -1208,6 +1216,11 @@
def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
for d in self.get("items"):
if cstr(d.t_warehouse):
+ if d.s_warehouse and d.serial_and_batch_bundle:
+ d.serial_and_batch_bundle = get_copy_of_serial_and_batch_bundle(
+ d.serial_and_batch_bundle, d.t_warehouse
+ )
+
sle = self.get_sl_entries(
d,
{
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 0df0a04..4ad6b26 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -32,9 +32,11 @@
"stock_uom",
"project",
"serial_and_batch_bundle",
+ "has_batch_no",
"batch_no",
"column_break_26",
"fiscal_year",
+ "has_serial_no",
"serial_no",
"is_cancelled",
"to_rename"
@@ -317,6 +319,20 @@
"label": "Serial and Batch Bundle",
"options": "Serial and Batch Bundle",
"search_index": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_batch_no",
+ "fieldname": "has_batch_no",
+ "fieldtype": "Check",
+ "label": "Has Batch No"
+ },
+ {
+ "default": "0",
+ "fetch_from": "item_code.has_serial_no",
+ "fieldname": "has_serial_no",
+ "fieldtype": "Check",
+ "label": "Has Serial No"
}
],
"hide_toolbar": 1,
@@ -325,7 +341,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2022-11-24 13:14:31.974743",
+ "modified": "2022-12-28 14:50:56.359348",
"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 1bcea69..c95d821 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
from frappe import _
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
-from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, get_link_to_form, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -47,6 +47,7 @@
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
+ self.process_serial_and_batch_bundle()
def on_submit(self):
self.check_stock_frozen_date()
@@ -103,15 +104,20 @@
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}"
- )
+ else:
+ bundle_data = frappe.get_cached_value(
+ "Serial and Batch Bundle", self.serial_and_batch_bundle, ["item_code", "docstatus"], as_dict=1
)
+ if self.item_code != bundle_data.item_code:
+ frappe.throw(
+ _(f"Serial and Batch Bundle {self.serial_and_batch_bundle} is not for Item {self.item_code}")
+ )
+
+ if bundle_data.docstatus != 1:
+ link = get_link_to_form("Serial and Batch Bundle", self.serial_and_batch_bundle)
+ frappe.throw(_(f"Serial and Batch Bundle {link} should be submitted first"))
+
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}"))
@@ -211,6 +217,36 @@
msg += "<br>" + "<br>".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
+ def process_serial_and_batch_bundle(self):
+ if self.serial_and_batch_bundle:
+ self.update_warehouse_and_voucher_no()
+ self.set_outgoing_rate()
+
+ def update_warehouse_and_voucher_no(self):
+ voucher_no = self.name if not self.is_cancelled else None
+ frappe.db.set_value(
+ "Serial and Batch Bundle", self.serial_and_batch_bundle, "voucher_no", voucher_no
+ )
+
+ if not self.is_cancelled:
+ frappe.db.sql(
+ f"""
+ UPDATE `tabSerial and Batch Ledger`
+ SET warehouse = {frappe.db.escape(self.warehouse)}
+ WHERE parent = {frappe.db.escape(self.serial_and_batch_bundle)}
+ AND (
+ warehouse is NULL or warehouse = '' or
+ warehouse != {frappe.db.escape(self.warehouse)}
+ )"""
+ )
+
+ def set_outgoing_rate(self):
+ if self.is_cancelled:
+ return
+
+ doc = frappe.get_cached_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
+ doc.set_outgoing_rate()
+
def on_cancel(self):
msg = _("Individual Stock Ledger Entry cannot be cancelled.")
msg += "<br>" + _("Please cancel related transaction.")
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 8d8b69d..525a0b0 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -60,8 +60,13 @@
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.validate_reserved_stock()
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Serial and Batch Bundle",
+ )
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index f3adefb..3b01287 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -19,7 +19,6 @@
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.setup.utils import get_exchange_rate
-from erpnext.stock.doctype.batch.batch import get_batch_no
from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_factor
from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no
from erpnext.stock.doctype.price_list.price_list import get_price_list_details
@@ -160,13 +159,6 @@
and out.warehouse
and out.stock_qty > 0
):
-
- if out.has_batch_no and not args.get("batch_no"):
- out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
- actual_batch_qty = get_batch_qty(out.batch_no, out.warehouse, out.item_code)
- if actual_batch_qty:
- out.update(actual_batch_qty)
-
if out.has_serial_no and args.get("batch_no"):
reserved_so = get_so_reservation_for_item(args)
out.batch_no = args.get("batch_no")
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index e3cbb43..4886755 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -295,19 +295,3 @@
"posting_time": posting_time,
}
)
-
-
-def reset_serial_no_status_and_warehouse(serial_nos=None):
- if not serial_nos:
- serial_nos = frappe.db.sql_list("""select name from `tabSerial No` where docstatus = 0""")
- for serial_no in serial_nos:
- try:
- sr = frappe.get_doc("Serial No", serial_no)
- last_sle = sr.get_last_sle()
- if flt(last_sle.actual_qty) > 0:
- sr.warehouse = last_sle.warehouse
-
- sr.via_stock_ledger = True
- sr.save()
- except Exception:
- pass
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2945c3d..e70e7f1 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -69,6 +69,9 @@
if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
+ if not cancel and sle["actual_qty"] > 0 and sle.get("serial_and_batch_bundle"):
+ set_incoming_rate_for_serial_and_batch(sle)
+
if cancel:
sle["actual_qty"] = -flt(sle.get("actual_qty"))
@@ -104,6 +107,18 @@
)
+def set_incoming_rate_for_serial_and_batch(row):
+ frappe.db.sql(
+ """
+ UPDATE `tabSerial and Batch Ledger`
+ SET incoming_rate = %s
+ WHERE
+ parent = %s
+ """,
+ (row.get("incoming_rate"), row.get("serial_and_batch_bundle")),
+ )
+
+
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
if not args.get("posting_date"):
@@ -659,8 +674,6 @@
self.new_items_found = True
def process_sle(self, sle):
- from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
@@ -692,7 +705,7 @@
):
sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
- if get_serial_nos(sle.serial_no):
+ if sle.serial_and_batch_bundle and sle.has_serial_no:
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
@@ -701,9 +714,7 @@
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
self.wh_data.valuation_rate
)
- elif sle.batch_no and frappe.db.get_value(
- "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
- ):
+ elif sle.serial_and_batch_bundle and sle.has_batch_no:
self.update_batched_values(sle)
else:
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@@ -963,9 +974,22 @@
item.db_update()
def get_serialized_values(self, sle):
- incoming_rate = flt(sle.incoming_rate)
+ ledger = frappe.db.get_value(
+ "Serial and Batch Bundle",
+ sle.serial_and_batch_bundle,
+ ["avg_rate", "total_amount", "total_qty"],
+ as_dict=True,
+ )
+
+ if flt(abs(ledger.total_qty)) - flt(abs(sle.actual_qty)) > 0.001:
+ msg = f"""Actual Qty in Serial and Batch Bundle
+ {sle.serial_and_batch_bundle} does not match with
+ Stock Ledger Entry {sle.name}"""
+
+ frappe.throw(_(msg))
+
actual_qty = flt(sle.actual_qty)
- serial_nos = cstr(sle.serial_no).split("\n")
+ incoming_rate = flt(ledger.avg_rate)
if incoming_rate < 0:
# wrong incoming rate
@@ -977,11 +1001,11 @@
else:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
+ outgoing_value = flt(ledger.total_amount)
if not sle.is_cancelled:
- outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
stock_value_change = -1 * outgoing_value
else:
- stock_value_change = actual_qty * sle.outgoing_rate
+ stock_value_change = outgoing_value
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
@@ -1138,7 +1162,7 @@
outgoing_rate = get_batch_incoming_rate(
item_code=sle.item_code,
warehouse=sle.warehouse,
- batch_no=sle.batch_no,
+ serial_and_batch_bundle=sle.serial_and_batch_bundle,
posting_date=sle.posting_date,
posting_time=sle.posting_time,
creation=sle.creation,
@@ -1402,10 +1426,11 @@
def get_batch_incoming_rate(
- item_code, warehouse, batch_no, posting_date, posting_time, creation=None
+ item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
):
sle = frappe.qb.DocType("Stock Ledger Entry")
+ batch_ledger = frappe.qb.DocType("Serial and Batch Ledger")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time
@@ -1416,18 +1441,36 @@
== CombineDatetime(posting_date, posting_time)
) & (sle.creation < creation)
+ batches = frappe.get_all(
+ "Serial and Batch Ledger", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
+ )
+
batch_details = (
frappe.qb.from_(sle)
- .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
+ .inner_join(batch_ledger)
+ .on(sle.serial_and_batch_bundle == batch_ledger.parent)
+ .select(
+ Sum(
+ Case()
+ .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
+ .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
+ ).as_("batch_value"),
+ Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
+ "batch_qty"
+ ),
+ )
.where(
(sle.item_code == item_code)
& (sle.warehouse == warehouse)
- & (sle.batch_no == batch_no)
+ & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
).run(as_dict=True)
+ print(batch_details)
+
+ print(batch_details[0].batch_value / batch_details[0].batch_qty)
if batch_details and batch_details[0].batch_qty:
return batch_details[0].batch_value / batch_details[0].batch_qty