|
@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc |
|
|
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) |
|
|
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) |
|
|
|
|
|
|
|
|
args = sle_doc.as_dict() |
|
|
args = sle_doc.as_dict() |
|
|
|
|
|
|
|
|
|
|
|
if sle.get("voucher_type") == "Stock Reconciliation": |
|
|
|
|
|
# preserve previous_qty_after_transaction for qty reposting |
|
|
|
|
|
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") |
|
|
|
|
|
|
|
|
update_bin(args, allow_negative_stock, via_landed_cost_voucher) |
|
|
update_bin(args, allow_negative_stock, via_landed_cost_voucher) |
|
|
|
|
|
|
|
|
def get_args_for_future_sle(row): |
|
|
def get_args_for_future_sle(row): |
|
@ -215,7 +220,7 @@ class update_entries_after(object): |
|
|
""" |
|
|
""" |
|
|
self.data.setdefault(args.warehouse, frappe._dict()) |
|
|
self.data.setdefault(args.warehouse, frappe._dict()) |
|
|
warehouse_dict = self.data[args.warehouse] |
|
|
warehouse_dict = self.data[args.warehouse] |
|
|
previous_sle = self.get_previous_sle_of_current_voucher(args) |
|
|
previous_sle = get_previous_sle_of_current_voucher(args) |
|
|
warehouse_dict.previous_sle = previous_sle |
|
|
warehouse_dict.previous_sle = previous_sle |
|
|
|
|
|
|
|
|
for key in ("qty_after_transaction", "valuation_rate", "stock_value"): |
|
|
for key in ("qty_after_transaction", "valuation_rate", "stock_value"): |
|
@ -227,29 +232,6 @@ class update_entries_after(object): |
|
|
"stock_value_difference": 0.0 |
|
|
"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 |
|
|
|
|
|
for update""", args, as_dict=1) |
|
|
|
|
|
|
|
|
|
|
|
return sle[0] if sle else frappe._dict() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build(self): |
|
|
def build(self): |
|
|
from erpnext.controllers.stock_controller import future_sle_exists |
|
|
from erpnext.controllers.stock_controller import future_sle_exists |
|
|
|
|
|
|
|
@ -734,6 +716,35 @@ class update_entries_after(object): |
|
|
bin_doc.flags.via_stock_ledger_entry = True |
|
|
bin_doc.flags.via_stock_ledger_entry = True |
|
|
bin_doc.save(ignore_permissions=True) |
|
|
bin_doc.save(ignore_permissions=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): |
|
|
|
|
|
"""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" |
|
|
|
|
|
|
|
|
|
|
|
voucher_condition = "" |
|
|
|
|
|
if exclude_current_voucher: |
|
|
|
|
|
voucher_no = args.get("voucher_no") |
|
|
|
|
|
voucher_condition = f"and voucher_no != '{voucher_no}'" |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
{voucher_condition} |
|
|
|
|
|
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 |
|
|
|
|
|
for update""".format(voucher_condition=voucher_condition), args, as_dict=1) |
|
|
|
|
|
|
|
|
|
|
|
return sle[0] if sle else frappe._dict() |
|
|
|
|
|
|
|
|
def get_previous_sle(args, for_update=False): |
|
|
def get_previous_sle(args, for_update=False): |
|
|
""" |
|
|
""" |
|
|
get the last sle on or before the current time-bucket, |
|
|
get the last sle on or before the current time-bucket, |
|
@ -862,9 +873,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, |
|
|
return valuation_rate |
|
|
return valuation_rate |
|
|
|
|
|
|
|
|
def update_qty_in_future_sle(args, allow_negative_stock=None): |
|
|
def update_qty_in_future_sle(args, allow_negative_stock=None): |
|
|
|
|
|
"""Recalculate Qty after Transaction in future SLEs based on current SLE.""" |
|
|
|
|
|
datetime_limit_condition = "" |
|
|
|
|
|
qty_shift = args.actual_qty |
|
|
|
|
|
|
|
|
|
|
|
# find difference/shift in qty caused by stock reconciliation |
|
|
|
|
|
if args.voucher_type == "Stock Reconciliation": |
|
|
|
|
|
qty_shift = get_stock_reco_qty_shift(args) |
|
|
|
|
|
|
|
|
|
|
|
# find the next nearest stock reco so that we only recalculate SLEs till that point |
|
|
|
|
|
next_stock_reco_detail = get_next_stock_reco(args) |
|
|
|
|
|
if next_stock_reco_detail: |
|
|
|
|
|
detail = next_stock_reco_detail[0] |
|
|
|
|
|
# add condition to update SLEs before this date & time |
|
|
|
|
|
datetime_limit_condition = get_datetime_limit_condition(detail) |
|
|
|
|
|
|
|
|
frappe.db.sql(""" |
|
|
frappe.db.sql(""" |
|
|
update `tabStock Ledger Entry` |
|
|
update `tabStock Ledger Entry` |
|
|
set qty_after_transaction = qty_after_transaction + {qty} |
|
|
set qty_after_transaction = qty_after_transaction + {qty_shift} |
|
|
where |
|
|
where |
|
|
item_code = %(item_code)s |
|
|
item_code = %(item_code)s |
|
|
and warehouse = %(warehouse)s |
|
|
and warehouse = %(warehouse)s |
|
@ -876,15 +902,70 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): |
|
|
and creation > %(creation)s |
|
|
and creation > %(creation)s |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
""".format(qty=args.actual_qty), args) |
|
|
{datetime_limit_condition} |
|
|
|
|
|
""".format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) |
|
|
|
|
|
|
|
|
validate_negative_qty_in_future_sle(args, allow_negative_stock) |
|
|
validate_negative_qty_in_future_sle(args, allow_negative_stock) |
|
|
|
|
|
|
|
|
|
|
|
def get_stock_reco_qty_shift(args): |
|
|
|
|
|
stock_reco_qty_shift = 0 |
|
|
|
|
|
if args.get("is_cancelled"): |
|
|
|
|
|
if args.get("previous_qty_after_transaction"): |
|
|
|
|
|
# get qty (balance) that was set at submission |
|
|
|
|
|
last_balance = args.get("previous_qty_after_transaction") |
|
|
|
|
|
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) |
|
|
|
|
|
else: |
|
|
|
|
|
stock_reco_qty_shift = flt(args.actual_qty) |
|
|
|
|
|
else: |
|
|
|
|
|
# reco is being submitted |
|
|
|
|
|
last_balance = get_previous_sle_of_current_voucher(args, |
|
|
|
|
|
exclude_current_voucher=True).get("qty_after_transaction") |
|
|
|
|
|
|
|
|
|
|
|
if last_balance is not None: |
|
|
|
|
|
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) |
|
|
|
|
|
else: |
|
|
|
|
|
stock_reco_qty_shift = args.qty_after_transaction |
|
|
|
|
|
|
|
|
|
|
|
return stock_reco_qty_shift |
|
|
|
|
|
|
|
|
|
|
|
def get_next_stock_reco(args): |
|
|
|
|
|
"""Returns next nearest stock reconciliaton's details.""" |
|
|
|
|
|
|
|
|
|
|
|
return frappe.db.sql(""" |
|
|
|
|
|
select |
|
|
|
|
|
name, posting_date, posting_time, creation, voucher_no |
|
|
|
|
|
from |
|
|
|
|
|
`tabStock Ledger Entry` |
|
|
|
|
|
where |
|
|
|
|
|
item_code = %(item_code)s |
|
|
|
|
|
and warehouse = %(warehouse)s |
|
|
|
|
|
and voucher_type = 'Stock Reconciliation' |
|
|
|
|
|
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 |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
limit 1 |
|
|
|
|
|
""", args, as_dict=1) |
|
|
|
|
|
|
|
|
|
|
|
def get_datetime_limit_condition(detail): |
|
|
|
|
|
return f""" |
|
|
|
|
|
and |
|
|
|
|
|
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') |
|
|
|
|
|
or ( |
|
|
|
|
|
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') |
|
|
|
|
|
and creation < '{detail.creation}' |
|
|
|
|
|
) |
|
|
|
|
|
)""" |
|
|
|
|
|
|
|
|
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): |
|
|
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): |
|
|
allow_negative_stock = allow_negative_stock \ |
|
|
allow_negative_stock = allow_negative_stock \ |
|
|
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) |
|
|
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) |
|
|
|
|
|
|
|
|
if args.actual_qty < 0 and not allow_negative_stock: |
|
|
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: |
|
|
sle = get_future_sle_with_negative_qty(args) |
|
|
sle = get_future_sle_with_negative_qty(args) |
|
|
if sle: |
|
|
if sle: |
|
|
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( |
|
|
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( |
|
|