mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 02:01:21 +00:00
fix: negative stock validation
This commit is contained in:
@@ -1026,7 +1026,14 @@ class TestDeliveryNote(IntegrationTestCase):
|
|||||||
def test_sales_invoice_qty_after_return(self):
|
def test_sales_invoice_qty_after_return(self):
|
||||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||||
|
|
||||||
dn = create_delivery_note(qty=10)
|
item = make_item(
|
||||||
|
"Test Sales Invoice Qty After Return",
|
||||||
|
properties={"is_stock_item": 1, "stock_uom": "Nos"},
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||||
|
|
||||||
|
dn = create_delivery_note(item_code=item.name, qty=10)
|
||||||
|
|
||||||
dnr1 = make_sales_return(dn.name)
|
dnr1 = make_sales_return(dn.name)
|
||||||
dnr1.get("items")[0].qty = -3
|
dnr1.get("items")[0].qty = -3
|
||||||
@@ -1042,8 +1049,8 @@ class TestDeliveryNote(IntegrationTestCase):
|
|||||||
self.assertEqual(si.get("items")[0].qty, 5)
|
self.assertEqual(si.get("items")[0].qty, 5)
|
||||||
|
|
||||||
si.reload().cancel().delete()
|
si.reload().cancel().delete()
|
||||||
dnr1.reload().cancel().delete()
|
|
||||||
dnr2.reload().cancel().delete()
|
dnr2.reload().cancel().delete()
|
||||||
|
dnr1.reload().cancel().delete()
|
||||||
dn.reload().cancel().delete()
|
dn.reload().cancel().delete()
|
||||||
|
|
||||||
def test_dn_billing_status_case3(self):
|
def test_dn_billing_status_case3(self):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, scrub
|
from frappe import _, bold, scrub
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.query_builder import Order
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_to_date,
|
add_to_date,
|
||||||
@@ -67,8 +68,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
from erpnext.controllers.stock_controller import future_sle_exists
|
from erpnext.controllers.stock_controller import future_sle_exists
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
cancel = sl_entries[0].get("is_cancelled")
|
cancelled = sl_entries[0].get("is_cancelled")
|
||||||
if cancel:
|
if cancelled:
|
||||||
validate_cancellation(sl_entries)
|
validate_cancellation(sl_entries)
|
||||||
set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
|
set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
if sle.serial_no and not via_landed_cost_voucher:
|
if sle.serial_no and not via_landed_cost_voucher:
|
||||||
validate_serial_no(sle)
|
validate_serial_no(sle)
|
||||||
|
|
||||||
if cancel:
|
if cancelled:
|
||||||
sle["actual_qty"] = -flt(sle.get("actual_qty"))
|
sle["actual_qty"] = -flt(sle.get("actual_qty"))
|
||||||
|
|
||||||
if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
|
if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
|
||||||
@@ -108,7 +109,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
if is_stock_item:
|
if is_stock_item:
|
||||||
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||||
args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
|
args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
|
||||||
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
|
repost_current_voucher(
|
||||||
|
args, allow_negative_stock, via_landed_cost_voucher, cancelled=cancelled
|
||||||
|
)
|
||||||
update_bin_qty(bin_name, args)
|
update_bin_qty(bin_name, args)
|
||||||
else:
|
else:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
@@ -116,7 +119,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False, cancelled=False):
|
||||||
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
|
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
|
||||||
if not args.get("posting_date"):
|
if not args.get("posting_date"):
|
||||||
args["posting_date"] = nowdate()
|
args["posting_date"] = nowdate()
|
||||||
@@ -135,6 +138,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
|
|||||||
"sle_id": args.get("name"),
|
"sle_id": args.get("name"),
|
||||||
"creation": args.get("creation"),
|
"creation": args.get("creation"),
|
||||||
"reserved_stock": args.get("reserved_stock"),
|
"reserved_stock": args.get("reserved_stock"),
|
||||||
|
"cancelled": cancelled,
|
||||||
},
|
},
|
||||||
allow_negative_stock=allow_negative_stock,
|
allow_negative_stock=allow_negative_stock,
|
||||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||||
@@ -667,33 +671,31 @@ class update_entries_after:
|
|||||||
def process_sle_against_current_timestamp(self):
|
def process_sle_against_current_timestamp(self):
|
||||||
sl_entries = self.get_sle_against_current_voucher()
|
sl_entries = self.get_sle_against_current_voucher()
|
||||||
for sle in sl_entries:
|
for sle in sl_entries:
|
||||||
|
sle["timestamp"] = sle.posting_datetime
|
||||||
self.process_sle(sle)
|
self.process_sle(sle)
|
||||||
|
|
||||||
def get_sle_against_current_voucher(self):
|
def get_sle_against_current_voucher(self):
|
||||||
self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time)
|
self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time)
|
||||||
|
doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
return frappe.db.sql(
|
query = (
|
||||||
"""
|
frappe.qb.from_(doctype)
|
||||||
select
|
.select("*")
|
||||||
*, posting_datetime as "timestamp"
|
.where(
|
||||||
from
|
(doctype.item_code == self.args.item_code)
|
||||||
`tabStock Ledger Entry`
|
& (doctype.warehouse == self.args.warehouse)
|
||||||
where
|
& (doctype.is_cancelled == 0)
|
||||||
item_code = %(item_code)s
|
& (doctype.posting_datetime == self.args.posting_datetime)
|
||||||
and warehouse = %(warehouse)s
|
)
|
||||||
and is_cancelled = 0
|
.orderby(doctype.creation, order=Order.asc)
|
||||||
and (
|
.for_update()
|
||||||
posting_datetime = %(posting_datetime)s
|
|
||||||
)
|
|
||||||
and creation = %(creation)s
|
|
||||||
order by
|
|
||||||
creation ASC
|
|
||||||
for update
|
|
||||||
""",
|
|
||||||
self.args,
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not self.args.get("cancelled"):
|
||||||
|
query = query.where(doctype.creation == self.args.creation)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
def get_future_entries_to_fix(self):
|
def get_future_entries_to_fix(self):
|
||||||
# includes current entry!
|
# includes current entry!
|
||||||
args = self.data[self.args.warehouse].previous_sle or frappe._dict(
|
args = self.data[self.args.warehouse].previous_sle or frappe._dict(
|
||||||
@@ -1715,7 +1717,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
|||||||
voucher_no = args.get("voucher_no")
|
voucher_no = args.get("voucher_no")
|
||||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||||
|
|
||||||
elif args.get("creation") and args.get("sle_id"):
|
elif args.get("creation") and args.get("sle_id") and not args.get("cancelled"):
|
||||||
creation = args.get("creation")
|
creation = args.get("creation")
|
||||||
operator = "<="
|
operator = "<="
|
||||||
voucher_condition = f"and creation < '{creation}'"
|
voucher_condition = f"and creation < '{creation}'"
|
||||||
|
|||||||
Reference in New Issue
Block a user