mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-17 16:45:02 +00:00
fix: negative stock for purchae return
(cherry picked from commit d68a04ad16)
# Conflicts:
# erpnext/stock/serial_batch_bundle.py
This commit is contained in:
committed by
Mergify
parent
69dc9e81d5
commit
f9fd0ffbae
@@ -78,7 +78,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
for ledger in entries:
|
for ledger in entries:
|
||||||
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
|
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
|
||||||
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
|
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
|
||||||
self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
|
|
||||||
|
|
||||||
@deprecated
|
@deprecated
|
||||||
def get_sle_for_batches(self):
|
def get_sle_for_batches(self):
|
||||||
@@ -231,7 +230,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
batch_data = query.run(as_dict=True)
|
batch_data = query.run(as_dict=True)
|
||||||
for d in batch_data:
|
for d in batch_data:
|
||||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||||
self.total_qty[d.batch_no] += flt(d.batch_qty)
|
|
||||||
|
|
||||||
for d in batch_data:
|
for d in batch_data:
|
||||||
if self.available_qty.get(d.batch_no):
|
if self.available_qty.get(d.batch_no):
|
||||||
@@ -332,7 +330,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
batch_data = query.run(as_dict=True)
|
batch_data = query.run(as_dict=True)
|
||||||
for d in batch_data:
|
for d in batch_data:
|
||||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||||
self.total_qty[d.batch_no] += flt(d.batch_qty)
|
|
||||||
|
|
||||||
if not self.last_sle:
|
if not self.last_sle:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4677,6 +4677,45 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
|
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
|
||||||
|
|
||||||
|
def test_negative_stock_error_for_purchase_return(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Negative Stock for Purchase Return Item",
|
||||||
|
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -3),
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -4),
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return_pr = make_return_doc("Purchase Receipt", pr.name)
|
||||||
|
self.assertRaises(frappe.ValidationError, return_pr.submit)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -576,14 +576,12 @@ class SerialandBatchBundle(Document):
|
|||||||
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
|
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
|
||||||
|
|
||||||
precision = d.precision("qty")
|
precision = d.precision("qty")
|
||||||
for field in ["available_qty", "total_qty"]:
|
available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision)
|
||||||
value = getattr(sn_obj, field)
|
if self.docstatus == 1:
|
||||||
available_qty = flt(value.get(d.batch_no), precision)
|
available_qty += flt(d.qty, precision)
|
||||||
if self.docstatus == 1:
|
|
||||||
available_qty += flt(d.qty, precision)
|
|
||||||
|
|
||||||
if not allow_negative_stock:
|
if not allow_negative_stock:
|
||||||
self.validate_negative_batch(d.batch_no, available_qty, field)
|
self.validate_negative_batch(d.batch_no, available_qty)
|
||||||
|
|
||||||
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
||||||
|
|
||||||
@@ -596,8 +594,8 @@ class SerialandBatchBundle(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_negative_batch(self, batch_no, available_qty, field=None):
|
def validate_negative_batch(self, batch_no, available_qty):
|
||||||
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field):
|
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
|
||||||
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
|
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
|
||||||
has negative stock
|
has negative stock
|
||||||
of quantity {bold(available_qty)} in the
|
of quantity {bold(available_qty)} in the
|
||||||
@@ -605,7 +603,7 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
frappe.throw(_(msg), BatchNegativeStockError)
|
frappe.throw(_(msg), BatchNegativeStockError)
|
||||||
|
|
||||||
def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None):
|
def is_stock_reco_for_valuation_adjustment(self, available_qty):
|
||||||
if (
|
if (
|
||||||
self.voucher_type == "Stock Reconciliation"
|
self.voucher_type == "Stock Reconciliation"
|
||||||
and self.type_of_transaction == "Outward"
|
and self.type_of_transaction == "Outward"
|
||||||
@@ -613,7 +611,6 @@ class SerialandBatchBundle(Document):
|
|||||||
and (
|
and (
|
||||||
abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
|
abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
|
||||||
== abs(available_qty)
|
== abs(available_qty)
|
||||||
or field == "total_qty"
|
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@@ -1346,6 +1343,7 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_serial_nos_inventory()
|
self.validate_serial_nos_inventory()
|
||||||
|
self.validate_batch_quantity()
|
||||||
|
|
||||||
def set_purchase_document_no(self):
|
def set_purchase_document_no(self):
|
||||||
if self.flags.ignore_validate_serial_batch:
|
if self.flags.ignore_validate_serial_batch:
|
||||||
@@ -1404,6 +1402,106 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_voucher_no_docstatus()
|
self.validate_voucher_no_docstatus()
|
||||||
|
self.validate_batch_quantity()
|
||||||
|
|
||||||
|
def validate_batch_quantity(self):
|
||||||
|
if not self.has_batch_no:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.type_of_transaction != "Outward" or (
|
||||||
|
self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
batch_wise_available_qty = self.get_batchwise_available_qty()
|
||||||
|
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||||
|
|
||||||
|
for d in self.entries:
|
||||||
|
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
|
||||||
|
if flt(available_qty, precision) < 0:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"""
|
||||||
|
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
|
||||||
|
).format(
|
||||||
|
bold(d.batch_no),
|
||||||
|
bold(self.item_code),
|
||||||
|
bold(self.warehouse),
|
||||||
|
bold(abs(flt(available_qty, precision))),
|
||||||
|
),
|
||||||
|
title=_("Negative Stock Error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_batchwise_available_qty(self):
|
||||||
|
available_qty = self.get_available_qty_from_sabb()
|
||||||
|
available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
|
||||||
|
|
||||||
|
if not available_qty_from_ledger:
|
||||||
|
return available_qty
|
||||||
|
|
||||||
|
for batch_no, qty in available_qty_from_ledger.items():
|
||||||
|
if batch_no in available_qty:
|
||||||
|
available_qty[batch_no] += qty
|
||||||
|
else:
|
||||||
|
available_qty[batch_no] = qty
|
||||||
|
|
||||||
|
return available_qty
|
||||||
|
|
||||||
|
def get_available_qty_from_stock_ledger(self):
|
||||||
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
|
|
||||||
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sle)
|
||||||
|
.select(
|
||||||
|
sle.batch_no,
|
||||||
|
Sum(sle.actual_qty).as_("available_qty"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(sle.item_code == self.item_code)
|
||||||
|
& (sle.warehouse == self.warehouse)
|
||||||
|
& (sle.is_cancelled == 0)
|
||||||
|
& (sle.batch_no.isin(batches))
|
||||||
|
& (sle.docstatus == 1)
|
||||||
|
& (sle.serial_and_batch_bundle.isnull())
|
||||||
|
& (sle.batch_no.isnotnull())
|
||||||
|
)
|
||||||
|
.for_update()
|
||||||
|
.groupby(sle.batch_no)
|
||||||
|
)
|
||||||
|
|
||||||
|
res = query.run(as_list=True)
|
||||||
|
|
||||||
|
return frappe._dict(res) if res else frappe._dict()
|
||||||
|
|
||||||
|
def get_available_qty_from_sabb(self):
|
||||||
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
|
|
||||||
|
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(child)
|
||||||
|
.select(
|
||||||
|
child.batch_no,
|
||||||
|
Sum(child.qty).as_("available_qty"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(child.item_code == self.item_code)
|
||||||
|
& (child.warehouse == self.warehouse)
|
||||||
|
& (child.is_cancelled == 0)
|
||||||
|
& (child.batch_no.isin(batches))
|
||||||
|
& (child.docstatus == 1)
|
||||||
|
& (child.type_of_transaction.isin(["Inward", "Outward"]))
|
||||||
|
)
|
||||||
|
.for_update()
|
||||||
|
.groupby(child.batch_no)
|
||||||
|
)
|
||||||
|
query = query.where(child.voucher_type != "Pick List")
|
||||||
|
|
||||||
|
res = query.run(as_list=True)
|
||||||
|
|
||||||
|
return frappe._dict(res) if res else frappe._dict()
|
||||||
|
|
||||||
def validate_voucher_no_docstatus(self):
|
def validate_voucher_no_docstatus(self):
|
||||||
if self.voucher_type == "POS Invoice":
|
if self.voucher_type == "POS Invoice":
|
||||||
|
|||||||
@@ -803,15 +803,19 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
for ledger in entries:
|
for ledger in entries:
|
||||||
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
|
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
|
||||||
self.available_qty[ledger.batch_no] += flt(ledger.qty)
|
self.available_qty[ledger.batch_no] += flt(ledger.qty)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
entries = self.get_batch_wise_total_available_qty()
|
entries = self.get_batch_wise_total_available_qty()
|
||||||
for row in entries:
|
for row in entries:
|
||||||
self.total_qty[row.batch_no] += flt(row.total_qty)
|
self.total_qty[row.batch_no] += flt(row.total_qty)
|
||||||
|
=======
|
||||||
|
>>>>>>> d68a04ad16 (fix: negative stock for purchae return)
|
||||||
|
|
||||||
self.calculate_avg_rate_from_deprecarated_ledgers()
|
self.calculate_avg_rate_from_deprecarated_ledgers()
|
||||||
self.calculate_avg_rate_for_non_batchwise_valuation()
|
self.calculate_avg_rate_for_non_batchwise_valuation()
|
||||||
self.set_stock_value_difference()
|
self.set_stock_value_difference()
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
def get_batch_wise_total_available_qty(self) -> list[dict]:
|
def get_batch_wise_total_available_qty(self) -> list[dict]:
|
||||||
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
|
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
|
||||||
if not self.batchwise_valuation_batches:
|
if not self.batchwise_valuation_batches:
|
||||||
@@ -851,6 +855,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
def get_batch_no_ledgers(self) -> list[dict]:
|
def get_batch_no_ledgers(self) -> list[dict]:
|
||||||
|
=======
|
||||||
|
def get_batch_stock_before_date(self) -> list[dict]:
|
||||||
|
>>>>>>> d68a04ad16 (fix: negative stock for purchae return)
|
||||||
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
|
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
|
||||||
if not self.batchwise_valuation_batches:
|
if not self.batchwise_valuation_batches:
|
||||||
return []
|
return []
|
||||||
|
|||||||
Reference in New Issue
Block a user