From 0222cd198d48634877cda4fbe3ae107d12eae6f9 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 15 Dec 2021 22:34:44 +0530 Subject: [PATCH 1/9] fix: Validation in POS for item batch no stock quantity (cherry picked from commit 9f235526d43658e333bc20bf8847bb8d21764332) --- .../doctype/pos_invoice/pos_invoice.py | 22 +++++++++- .../doctype/pos_invoice/test_pos_invoice.py | 43 ++++++++++++++++++- erpnext/stock/doctype/batch/batch.py | 26 +++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 814372f6b35..dffcbb7c0d4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account +from erpnext.stock.doctype.batch.batch import get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos @@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) elif invalid_serial_nos: - frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") + frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + def validate_pos_reserved_batch_qty(self, item): + filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} + + available_batch_qty = frappe.db.get_value('Batch', item.batch_no, 'batch_qty') + reserved_batch_qty = get_pos_reserved_batch_qty(filters) + + bold_item_name = frappe.bold(item.item_name) + bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty)) + bold_invalid_batch_no = frappe.bold(item.batch_no) + + if (available_batch_qty - reserved_batch_qty) == 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") + .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) + elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") + .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) + def validate_delivered_serial_nos(self, item): serial_nos = get_serial_nos(item.serial_no) delivered_serial_nos = frappe.db.get_list('Serial No', { @@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + elif d.batch_no: + self.validate_pos_reserved_batch_qty(d) else: if allow_negative_stock: return diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 66963335376..666e3f3e402 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -521,6 +521,41 @@ class TestPOSInvoice(unittest.TestCase): rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) + def test_pos_batch_ietm_qty_validation(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01') + item = frappe.get_doc('Item', '_BATCH ITEM') + batch = frappe.get_doc('Batch', 'TestBatch 01') + batch.submit() + item.batch_no = 'TestBatch 01' + item.save() + + se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01') + + pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) + pos_inv1.items[0].batch_no = 'TestBatch 01' + pos_inv1.save() + pos_inv1.submit() + + pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) + pos_inv2.items[0].batch_no = 'TestBatch 01' + pos_inv2.save() + + self.assertRaises(frappe.ValidationError, pos_inv2.submit) + + #teardown + pos_inv1.reload() + pos_inv1.cancel() + pos_inv1.delete() + pos_inv2.reload() + pos_inv2.delete() + se.cancel() + batch.reload() + batch.cancel() + batch.delete() + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None @@ -557,7 +592,8 @@ def create_pos_invoice(**args): "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no + "serial_no": args.serial_no, + "batch_no": args.batch_no }) if not args.do_not_save: @@ -570,3 +606,8 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv + +def make_batch_item(item_name): + from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): + return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3ce2d87f711..a3ba7e8d91f 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -313,3 +313,29 @@ def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" frappe.get_doc(args).insert().name + +@frappe.whitelist() +def get_pos_reserved_batch_qty(filters): + import json + + if isinstance(filters, str): + filters = json.loads(filters) + + pos_transacted_batch_nos = frappe.db.sql("""select item.qty + from `tabPOS Invoice` p, `tabPOS Invoice Item` item + where p.name = item.parent + and p.consolidated_invoice is NULL + and p.status != "Consolidated" + and p.docstatus = 1 + and item.docstatus = 1 + and item.item_code = %(item_code)s + and item.warehouse = %(warehouse)s + and item.batch_no = %(batch_no)s + + """, filters, as_dict=1) + + reserved_batch_qty = 0.0 + for d in pos_transacted_batch_nos: + reserved_batch_qty += d.qty + + return reserved_batch_qty \ No newline at end of file From 3947d859c5e514a37fde770da1c673148cbd461f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Thu, 16 Dec 2021 13:26:21 +0530 Subject: [PATCH 2/9] fix: replaced sql query with frappe.qb (cherry picked from commit 68beee2a00dba2b08ca3265f509f60d636b2dcb8) --- erpnext/stock/doctype/batch/batch.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index a3ba7e8d91f..b91320f67be 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -321,18 +321,19 @@ def get_pos_reserved_batch_qty(filters): if isinstance(filters, str): filters = json.loads(filters) - pos_transacted_batch_nos = frappe.db.sql("""select item.qty - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.status != "Consolidated" - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.batch_no = %(batch_no)s + p = frappe.qb.DocType("POS Invoice").as_("p") + item = frappe.qb.DocType("POS Invoice Item").as_("item") - """, filters, as_dict=1) + pos_transacted_batch_nos = frappe.qb.from_(p).from_(item).select(item.qty).where( + (p.name == item.parent) & + (p.consolidated_invoice.isnull()) & + (p.status != "Consolidated") & + (p.docstatus == 1) & + (item.docstatus == 1) & + (item.item_code == filters.get('item_code')) & + (item.warehouse == filters.get('warehouse')) & + (item.batch_no == filters.get('batch_no')) + ).run(as_dict=True) reserved_batch_qty = 0.0 for d in pos_transacted_batch_nos: From bf92681fff397d01d318506e5f9ff69870049231 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 21 Dec 2021 17:01:10 +0530 Subject: [PATCH 3/9] fix: typo (cherry picked from commit 9c8f5235374d19a03a8f7f03e0ca73a7040568b7) --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 666e3f3e402..7d31e0aa195 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -521,7 +521,7 @@ class TestPOSInvoice(unittest.TestCase): rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) - def test_pos_batch_ietm_qty_validation(self): + def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_batch_item_with_batch, ) From 0be144c1932540c95d3a7804e65c31a4c6e2f395 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 21 Dec 2021 17:05:36 +0530 Subject: [PATCH 4/9] chore: remove extra whitespace (cherry picked from commit c3a9ca09d15893ca48e405617a9a5bc215ae92ba) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index dffcbb7c0d4..0aeaae0dbf4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -143,7 +143,7 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: - frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") + frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) def validate_delivered_serial_nos(self, item): From 80f92ee547f90221b0599a579d13242a435b925c Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 16:40:11 +0530 Subject: [PATCH 5/9] fix: added Sum() in query (cherry picked from commit 267da4888900c7c2b6796637dc19d68ff442f07f) --- erpnext/stock/doctype/batch/batch.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b91320f67be..1df645dc69f 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -323,8 +323,9 @@ def get_pos_reserved_batch_qty(filters): p = frappe.qb.DocType("POS Invoice").as_("p") item = frappe.qb.DocType("POS Invoice Item").as_("item") + sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") - pos_transacted_batch_nos = frappe.qb.from_(p).from_(item).select(item.qty).where( + reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( (p.name == item.parent) & (p.consolidated_invoice.isnull()) & (p.status != "Consolidated") & @@ -333,10 +334,6 @@ def get_pos_reserved_batch_qty(filters): (item.item_code == filters.get('item_code')) & (item.warehouse == filters.get('warehouse')) & (item.batch_no == filters.get('batch_no')) - ).run(as_dict=True) + ).run() - reserved_batch_qty = 0.0 - for d in pos_transacted_batch_nos: - reserved_batch_qty += d.qty - - return reserved_batch_qty \ No newline at end of file + return reserved_batch_qty[0][0] \ No newline at end of file From 110a1e5557ed8101405922012e063b7f51e589f8 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 16:57:00 +0530 Subject: [PATCH 6/9] fix: using get_batch_qty method to get available_qty (cherry picked from commit 73854972198e99ba529651211ad264e9c50ea5ab) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 4 ++-- erpnext/stock/doctype/batch/batch.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0aeaae0dbf4..d5961223358 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,7 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.stock.doctype.batch.batch import get_pos_reserved_batch_qty +from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos @@ -132,7 +132,7 @@ class POSInvoice(SalesInvoice): def validate_pos_reserved_batch_qty(self, item): filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} - available_batch_qty = frappe.db.get_value('Batch', item.batch_no, 'batch_qty') + available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) reserved_batch_qty = get_pos_reserved_batch_qty(filters) bold_item_name = frappe.bold(item.item_name) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1df645dc69f..3b01e7484cf 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -336,4 +336,5 @@ def get_pos_reserved_batch_qty(filters): (item.batch_no == filters.get('batch_no')) ).run() - return reserved_batch_qty[0][0] \ No newline at end of file + flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) + return flt_reserved_batch_qty \ No newline at end of file From a34ded79f3482b53e243ce0b7cf714381b5417e2 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:01:57 +0530 Subject: [PATCH 7/9] fix: importing Sum() for qb for test fix --- erpnext/stock/doctype/batch/batch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3b01e7484cf..b32033c771b 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -316,6 +316,7 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): + from frappe.query_builder.functions import Sum import json if isinstance(filters, str): @@ -323,7 +324,7 @@ def get_pos_reserved_batch_qty(filters): p = frappe.qb.DocType("POS Invoice").as_("p") item = frappe.qb.DocType("POS Invoice Item").as_("item") - sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") + sum_qty = Sum(item.qty).as_("qty") reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( (p.name == item.parent) & @@ -337,4 +338,4 @@ def get_pos_reserved_batch_qty(filters): ).run() flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) - return flt_reserved_batch_qty \ No newline at end of file + return flt_reserved_batch_qty From 860a0f469613e60b4e417925d0e021771d69f1e4 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:06:40 +0530 Subject: [PATCH 8/9] fix: linters fix --- erpnext/stock/doctype/batch/batch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b32033c771b..a6f07f8237b 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -316,9 +316,9 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): - from frappe.query_builder.functions import Sum import json - + from frappe.query_builder.functions import Sum + if isinstance(filters, str): filters = json.loads(filters) From fe3ebafea444a612fb8975f848e2db3ab6cae600 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:10:03 +0530 Subject: [PATCH 9/9] fix: linters fix --- erpnext/stock/doctype/batch/batch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index a6f07f8237b..2fcdba59e35 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -317,8 +317,9 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): import json + from frappe.query_builder.functions import Sum - + if isinstance(filters, str): filters = json.loads(filters)