fix: update qty in future sle (#24649)

* fix: update qty in future sle

* fix: validate cancellation due to ongoing reposting

* fix: process sle against current timestamp
This commit is contained in:
Nabin Hait 2021-02-18 14:14:21 +05:30
parent deddcc513d
commit 186a045e28
8 changed files with 102 additions and 28 deletions

View File

@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1)
bin1_on_submit = get_bin(item, warehouse)
reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
cint(bin1_on_submit.reserved_qty_for_production))
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100)
@ -111,7 +111,7 @@ class TestWorkOrder(unittest.TestCase):
bin1_at_completion = get_bin(item, warehouse)
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
cint(bin1_on_submit.reserved_qty_for_production) - 1)
reserved_qty_on_submission - 1)
def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)

View File

@ -70,4 +70,4 @@ def get_warehouse_account(warehouse, warehouse_account=None):
return account
def get_company_default_inventory_account(company):
return frappe.get_cached_value('Company', company, 'default_inventory_account')
return frappe.get_cached_value('Company', company, 'default_inventory_account')

View File

@ -16,8 +16,9 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
@ -34,11 +35,13 @@ class Bin(Document):
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.name
"sle_id": args.name,
"creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# Validate negative qty in future transactions
validate_negative_qty_in_future_sle(args)
# update qty in future ale and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def update_qty(self, args):
# update the stock values (for current quantities)

View File

@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase):
def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit()

View File

@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase):
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "stock_value")
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
if existing_bin_qty < 0:
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt()

View File

@ -46,6 +46,9 @@ class RepostItemValuation(Document):
def repost(doc):
try:
if not frappe.db.exists("Repost Item Valuation", doc.name):
return
doc.set_status('In Progress')
frappe.db.commit()

View File

@ -37,6 +37,7 @@ class StockLedgerEntry(Document):
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
def on_submit(self):
self.check_stock_frozen_date()
self.actual_amt_check()

View File

@ -23,6 +23,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
cancel = sl_entries[0].get("is_cancelled")
if cancel:
validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
@ -45,6 +46,20 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def validate_cancellation(args):
if args[0].get("is_cancelled"):
repost_entry = frappe.db.get_value("Repost Item Valuation", {
'voucher_type': args[0].voucher_type,
'voucher_no': args[0].voucher_no,
'docstatus': 1
}, ['name', 'status'], as_dict=1)
if repost_entry:
if repost_entry.status == 'In Progress':
frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
if repost_entry.status == 'Queued':
frappe.delete_doc("Repost Item Valuation", repost_entry.name)
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@ -74,7 +89,8 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
"item_code": args[i].item_code,
"warehouse": args[i].warehouse,
"posting_date": args[i].posting_date,
"posting_time": args[i].posting_time
"posting_time": args[i].posting_time,
"creation": args[i].get("creation")
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
for item_wh, new_sle in iteritems(obj.new_items):
@ -86,7 +102,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
def get_args_for_voucher(voucher_type, voucher_no):
return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time"],
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse"
)
@ -155,7 +171,7 @@ class update_entries_after(object):
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
previous_sle = self.get_sle_before_datetime(args)
previous_sle = self.get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@ -167,9 +183,35 @@ class update_entries_after(object):
"stock_value_difference": 0.0
})
def get_previous_sle_of_current_voucher(self, args):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args['time_format'] = '%H:%i:%s'
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
sle = frappe.db.sql("""
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1""", args, as_dict=1)
return sle[0] if sle else frappe._dict()
def build(self):
from erpnext.controllers.stock_controller import check_if_future_sle_exists
if self.args.get("sle_id"):
self.process_sle_against_current_voucher()
self.process_sle_against_current_timestamp()
if not check_if_future_sle_exists(self.args):
self.update_bin()
else:
entries_to_fix = self.get_future_entries_to_fix()
@ -183,12 +225,12 @@ class update_entries_after(object):
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
self.update_bin()
if self.exceptions:
self.raise_exceptions()
self.update_bin()
def process_sle_against_current_voucher(self):
def process_sle_against_current_timestamp(self):
sl_entries = self.get_sle_against_current_voucher()
for sle in sl_entries:
self.process_sle(sle)
@ -204,8 +246,8 @@ class update_entries_after(object):
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_type = %(voucher_type)s
and voucher_no = %(voucher_no)s
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by
creation ASC
for update
@ -232,7 +274,6 @@ class update_entries_after(object):
return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle \
@ -639,7 +680,6 @@ class update_entries_after(object):
# update bin for each warehouse
for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse)
bin_doc.update({
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
@ -765,6 +805,25 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
frappe.db.sql("""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
)
)
""".format(qty=args.actual_qty), args)
validate_negative_qty_in_future_sle(args, allow_negative_stock)
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@ -793,7 +852,7 @@ def get_future_sle_with_negative_qty(args):
and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0
and qty_after_transaction + {0} < 0
and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
""".format(args.actual_qty), args, as_dict=1)
""", args, as_dict=1)