mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-23 06:59:20 +00:00
Merge pull request #29112 from frappe/mergify/bp/version-13-hotfix/pr-28907
fix: Validation in POS for item batch no stock quantity (backport #28907)
This commit is contained in:
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
|||||||
update_multi_mode_option,
|
update_multi_mode_option,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_due_date, get_party_account
|
from erpnext.accounts.party import get_due_date, get_party_account
|
||||||
|
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
|
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.")
|
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"))
|
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||||
elif invalid_serial_nos:
|
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"))
|
.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 = 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)
|
||||||
|
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):
|
def validate_delivered_serial_nos(self, item):
|
||||||
serial_nos = get_serial_nos(item.serial_no)
|
serial_nos = get_serial_nos(item.serial_no)
|
||||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||||
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
|
|||||||
if d.serial_no:
|
if d.serial_no:
|
||||||
self.validate_pos_reserved_serial_nos(d)
|
self.validate_pos_reserved_serial_nos(d)
|
||||||
self.validate_delivered_serial_nos(d)
|
self.validate_delivered_serial_nos(d)
|
||||||
|
elif d.batch_no:
|
||||||
|
self.validate_pos_reserved_batch_qty(d)
|
||||||
else:
|
else:
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -521,6 +521,41 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||||
self.assertEqual(rounded_total, 400)
|
self.assertEqual(rounded_total, 400)
|
||||||
|
|
||||||
|
def test_pos_batch_item_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):
|
def create_pos_invoice(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
pos_profile = None
|
pos_profile = None
|
||||||
@@ -557,7 +592,8 @@ def create_pos_invoice(**args):
|
|||||||
"income_account": args.income_account or "Sales - _TC",
|
"income_account": args.income_account or "Sales - _TC",
|
||||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||||
"cost_center": args.cost_center or "_Test Cost Center - _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:
|
if not args.do_not_save:
|
||||||
@@ -570,3 +606,8 @@ def create_pos_invoice(**args):
|
|||||||
pos_inv.payment_schedule = []
|
pos_inv.payment_schedule = []
|
||||||
|
|
||||||
return pos_inv
|
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))
|
||||||
@@ -313,3 +313,30 @@ def make_batch(args):
|
|||||||
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
||||||
args.doctype = "Batch"
|
args.doctype = "Batch"
|
||||||
frappe.get_doc(args).insert().name
|
frappe.get_doc(args).insert().name
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
p = frappe.qb.DocType("POS Invoice").as_("p")
|
||||||
|
item = frappe.qb.DocType("POS Invoice Item").as_("item")
|
||||||
|
sum_qty = Sum(item.qty).as_("qty")
|
||||||
|
|
||||||
|
reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_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()
|
||||||
|
|
||||||
|
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
|
||||||
|
return flt_reserved_batch_qty
|
||||||
|
|||||||
Reference in New Issue
Block a user