diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 868a150edf..8ed11a4299 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1448,6 +1448,7 @@ class PurchaseInvoice(BuyingController): "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 7454332cd3..714f24a789 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -400,6 +400,7 @@ class SalesInvoice(SellingController): "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 7687aad8b8..bd4bc18fb8 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 @@ class SellingController(StockController): "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 @@ class SellingController(StockController): "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 6156abad31..6e71004374 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -354,6 +354,7 @@ class StockController(AccountsController): "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 75845226a6..e30a302893 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.manufacturing.doctype.manufacturing_settings.manufacturing_settings 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 @@ class WorkOrder(Document): 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 b4676c1207..52abbc0a3d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -6,6 +6,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe 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 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe 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.TransactionController = class TransactionController extends erpnext.taxe } }; -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 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ 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 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ } 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 90967d93b5..fcaaaf0953 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 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { } 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,12 +698,14 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { }); } - 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', @@ -698,6 +717,41 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { 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 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { } } }) - } else if (this.item.has_batch_no) { + } + + if (this.item.has_batch_no) { fields = [ { fieldtype: 'Link', @@ -742,6 +798,38 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { 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 e3de49c57d..f5268d6e5e 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -420,6 +420,40 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran }); } + 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 2ee372e155..a647a17f80 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -12,7 +12,6 @@ from frappe.utils import cint, flt 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 @@ class DeliveryNote(SellingController): 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 @@ class DeliveryNote(SellingController): 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 3853bd1455..3f778696ff 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", @@ -507,16 +507,6 @@ "fieldname": "section_break_40", "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", @@ -542,15 +532,6 @@ "read_only": 1, "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", @@ -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 c5fb2411c2..244c905ca3 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 e0cb8ca021..312c166f8b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -7,8 +7,6 @@ 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'); 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 085e33db13..f16a72b2b8 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 @@ frappe.ui.form.on('Serial and Batch Bundle', { 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 @@ frappe.ui.form.on('Serial and Batch Bundle', { }; }); + 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 @@ frappe.ui.form.on('Serial and Batch Bundle', { } }; }); - }, - - 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 a08ed83013..cfe35d7755 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" ], @@ -90,12 +96,6 @@ "options": "Serial and Batch Ledger", "reqd": 1 }, - { - "fieldname": "qty", - "fieldtype": "Float", - "label": "Total Qty", - "read_only": 1 - }, { "fieldname": "voucher_type", "fieldtype": "Link", @@ -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 554c032f04..1c9dc15088 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 @@ class SerialandBatchBundle(Document): 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 @@ def update_serial_batch_no_ledgers(ledgers, child_row) -> object: 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 7fa9574494..65eaa0357e 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, @@ -46,22 +52,53 @@ "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" + }, + { + "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 7989b1ac75..7f22af16a1 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": [ @@ -90,29 +70,6 @@ "options": "Item", "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" @@ -150,84 +107,6 @@ "options": "Brand", "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", @@ -283,67 +162,6 @@ "options": "DocType", "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", @@ -408,6 +226,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_list_view": 1, "label": "Company", "options": "Company", "remember_last_selected_value": 1, @@ -415,25 +234,30 @@ "search_index": 1, "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 98beda0534..6d92cc3a76 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -9,17 +9,7 @@ import frappe 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 @@ class SerialNo(StockController): ) 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 @@ class SerialNo(StockController): 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 @@ class SerialNo(StockController): _("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 @@ def validate_serial_no(sle, item_det): 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 @@ def validate_serial_no(sle, item_det): _("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 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): 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 @@ def update_serial_nos(sle, item_det): 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 @@ def update_serial_nos(sle, item_det): ) 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 create_batch_for_serial_no(sle): 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 @@ def auto_create_serial_nos(sle, item_details) -> List[str]: 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 @@ def auto_create_serial_nos(sle, item_details) -> List[str]: "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 @@ def get_new_serial_number(series): 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:

{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 """
@@ -773,36 +640,8 @@ def clean_serial_no_string(serial_no: str) -> str: 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 7526d1d8a5..0000000000 --- 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 55b950b9db..3263ed43ff 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.brand.brand import get_brand_defaults 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 @@ class StockEntry(StockController): 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 @@ class StockEntry(StockController): 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 0df0a0416c..4ad6b26723 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 1bcea69dff..c95d821cf4 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 @@ import frappe 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 @@ class StockLedgerEntry(Document): 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 @@ class StockLedgerEntry(Document): 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 @@ class StockLedgerEntry(Document): msg += "
" + "
".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 += "
" + _("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 8d8b69de01..525a0b02c2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -60,8 +60,13 @@ class StockReconciliation(StockController): 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 f3adefb3e7..3b01287ab6 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -19,7 +19,6 @@ from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( 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 @@ def update_stock(args, out): 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 e3cbb43d8b..488675518a 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -295,19 +295,3 @@ def set_stock_balance_as_per_serial_no( "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 2945c3d731..e70e7f11aa 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -69,6 +69,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc 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 make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc ) +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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): ): 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): 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_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): 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 @@ def get_batch_incoming_rate( == 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