From 1e8f6c0840b0ea3f77df0c2e63e551c689b1b5a4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 3 Jul 2023 17:28:55 +0530 Subject: [PATCH] fix: reserve the pos invoice batches --- .../doctype/pos_invoice/pos_invoice.js | 2 +- .../doctype/pos_invoice/pos_invoice.py | 2 +- .../doctype/pos_invoice/test_pos_invoice.py | 33 +++++ .../serial_and_batch_bundle.py | 134 +++++++++++++----- 4 files changed, 135 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index cced37589b..32e267f33c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -20,7 +20,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex onload(doc) { super.onload(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry', 'Serial and Batch Bundle']; if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { this.frm.script_manager.trigger("is_pos"); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index bf393c0d29..4b2fcec757 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -93,7 +93,7 @@ class POSInvoice(SalesInvoice): ) def on_cancel(self): - self.ignore_linked_doctypes = "Payment Ledger Entry" + self.ignore_linked_doctypes = ["Payment Ledger Entry", "Serial and Batch Bundle"] # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() if not self.is_return and self.loyalty_program: diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index f842a16b74..0fce61f1e7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -767,6 +767,39 @@ class TestPOSInvoice(unittest.TestCase): ) self.assertEqual(rounded_total, 400) + def test_pos_batch_reservation(self): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_auto_batch_nos, + ) + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + + create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02") + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="_BATCH ITEM Test For Reserve", + qty=20, + basic_rate=100, + batch_no="TestBatch-RS 02", + ) + + pos_inv1 = create_pos_invoice( + item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02" + ) + pos_inv1.save() + pos_inv1.submit() + + batches = get_auto_batch_nos( + frappe._dict( + {"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"} + ) + ) + + for batch in batches: + if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC": + self.assertEqual(batch.qty, 5) + def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( BatchNegativeStockError, 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 57bb71ef1e..a5c7879b17 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 @@ -1241,59 +1241,125 @@ def get_reserved_serial_nos_for_pos(kwargs): return list(set(ignore_serial_nos) - set(returned_serial_nos)) +def get_reserved_batches_for_pos(kwargs): + pos_batches = frappe._dict() + pos_invoices = frappe.get_all( + "POS Invoice", + fields=[ + "`tabPOS Invoice Item`.batch_no", + "`tabPOS Invoice`.is_return", + "`tabPOS Invoice Item`.warehouse", + "`tabPOS Invoice Item`.name as child_docname", + "`tabPOS Invoice`.name as parent_docname", + "`tabPOS Invoice Item`.serial_and_batch_bundle", + ], + filters=[ + ["POS Invoice", "consolidated_invoice", "is", "not set"], + ["POS Invoice", "docstatus", "=", 1], + ["POS Invoice Item", "item_code", "=", kwargs.item_code], + ["POS Invoice", "name", "!=", kwargs.ignore_voucher_no], + ], + ) + + ids = [ + pos_invoice.serial_and_batch_bundle + for pos_invoice in pos_invoices + if pos_invoice.serial_and_batch_bundle + ] + + if not ids: + return [] + + if ids: + for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): + if d.batch_no not in pos_batches: + pos_batches[d.batch_no] = frappe._dict( + { + "qty": d.qty, + "warehouse": d.warehouse, + } + ) + else: + pos_batches[d.batch_no].qty += d.qty + + for row in pos_invoices: + if not row.batch_no: + continue + + if row.batch_no in pos_batches: + pos_batches[row.batch_no] -= row.qty * -1 if row.is_return else row.qty + else: + pos_batches[row.batch_no] = frappe._dict( + { + "qty": (row.qty * -1 if row.is_return else row.qty), + "warehouse": row.warehouse, + } + ) + + return pos_batches + + def get_auto_batch_nos(kwargs): available_batches = get_available_batches(kwargs) qty = flt(kwargs.qty) + pos_invoice_batches = get_reserved_batches_for_pos(kwargs) stock_ledgers_batches = get_stock_ledgers_batches(kwargs) - if stock_ledgers_batches: - update_available_batches(available_batches, stock_ledgers_batches) + if stock_ledgers_batches or pos_invoice_batches: + update_available_batches(available_batches, stock_ledgers_batches, pos_invoice_batches) available_batches = list(filter(lambda x: x.qty > 0, available_batches)) - if not qty: return available_batches + return get_qty_based_available_batches(available_batches, qty) + + +def get_qty_based_available_batches(available_batches, qty): batches = [] for batch in available_batches: - if qty > 0: - batch_qty = flt(batch.qty) - if qty > batch_qty: - batches.append( - frappe._dict( - { - "batch_no": batch.batch_no, - "qty": batch_qty, - "warehouse": batch.warehouse, - } - ) + if qty <= 0: + break + + batch_qty = flt(batch.qty) + if qty > batch_qty: + batches.append( + frappe._dict( + { + "batch_no": batch.batch_no, + "qty": batch_qty, + "warehouse": batch.warehouse, + } ) - qty -= batch_qty - else: - batches.append( - frappe._dict( - { - "batch_no": batch.batch_no, - "qty": qty, - "warehouse": batch.warehouse, - } - ) + ) + qty -= batch_qty + else: + batches.append( + frappe._dict( + { + "batch_no": batch.batch_no, + "qty": qty, + "warehouse": batch.warehouse, + } ) - qty = 0 + ) + qty = 0 return batches -def update_available_batches(available_batches, reserved_batches): - for batch_no, data in reserved_batches.items(): - batch_not_exists = True - for batch in available_batches: - if batch.batch_no == batch_no: - batch.qty += data.qty - batch_not_exists = False +def update_available_batches(available_batches, reserved_batches=None, pos_invoice_batches=None): + for batches in [reserved_batches, pos_invoice_batches]: + if batches: + for batch_no, data in batches.items(): + batch_not_exists = True + for batch in available_batches: + if batch.batch_no == batch_no and batch.warehouse == data.warehouse: + batch.qty += data.qty + batch_not_exists = False - if batch_not_exists: - available_batches.append(data) + if batch_not_exists: + available_batches.append(data) def get_available_batches(kwargs):