stock reco and utility function of stocks

This commit is contained in:
Nabin Hait
2013-01-08 18:29:24 +05:30
parent 418d580a8b
commit 902e8609e5
10 changed files with 514 additions and 333 deletions

View File

@@ -1,10 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
def execute(): def execute():
import webnotes import webnotes
from webnotes.model.code import get_obj res = webnotes.conn.sql("""select distinct item_code, warehouse from `tabStock Ledger Entry`
where posting_time > '00:00:00' and posting_time < '00:01:00'""", as_dict=1)
bins = webnotes.conn.sql("select distinct t2.name from `tabStock Ledger Entry` t1, tabBin t2 where t1.posting_time > '00:00:00' and t1.posting_time < '00:01:00' and t1.item_code = t2.item_code and t1.warehouse = t2.warehouse")
webnotes.conn.sql("update `tabStock Ledger Entry` set posting_time = '00:00:00' where posting_time > '00:00:00' and posting_time < '00:01:00'") webnotes.conn.sql("update `tabStock Ledger Entry` set posting_time = '00:00:00' where posting_time > '00:00:00' and posting_time < '00:01:00'")
for d in bins: from stock.stock_ledger import update_entries_after
get_obj('Bin', d[0]).update_entries_after(posting_date = '2000-01-01', posting_time = '12:01') for d in res:
update_entries_after({
item_code: d.item_code,
warehouse: d.warehouse,
posting_date: '2000-01-01',
posting_time: '12:01'
})

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import webnotes import webnotes
from stock.stock_ledger import update_entries_after
def execute(): def execute():
# add index # add index
@@ -80,7 +81,12 @@ def cleanup_wrong_sle():
for d in sle: for d in sle:
webnotes.conn.sql("update `tabStock Ledger Entry` set is_cancelled = 'Yes' where name = %s", d[3]) webnotes.conn.sql("update `tabStock Ledger Entry` set is_cancelled = 'Yes' where name = %s", d[3])
create_comment(d[3]) create_comment(d[3])
repost_bin(d[0], d[1]) update_entries_after({
item_code: d[0],
warehouse: d[1],
posting_date: "2012-07-01",
posting_time: "12:05"
})
def create_comment(dn): def create_comment(dn):
from webnotes.model.doc import Document from webnotes.model.doc import Document
@@ -92,10 +98,3 @@ def create_comment(dn):
cmt.comment_docname = dn cmt.comment_docname = dn
cmt.save(1) cmt.save(1)
def repost_bin(item, wh):
from webnotes.model.code import get_obj
bin = webnotes.conn.sql("select name from `tabBin` \
where item_code = %s and warehouse = %s", (item, wh))
get_obj('Bin', bin[0][0]).update_entries_after(posting_date = '2012-07-01', posting_time = '12:05')

View File

@@ -17,12 +17,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
def execute(): def execute():
import webnotes import webnotes
from webnotes.model.code import get_obj from stock.stock_ledger import update_entries_after
bin = webnotes.conn.sql("select name from `tabBin`") res = webnotes.conn.sql("select distinct item_code, warehouse from `tabStock Ledger Entry`")
i=0 i=0
for d in bin: for d in res:
try: try:
get_obj('Bin', d[0]).update_entries_after('2000-01-01', '12:05') update_entries_after({
item_code: d[0],
warehouse: d[1],
posting_date: "2000-01-01",
posting_time: "12:00"
})
except: except:
pass pass
i += 1 i += 1

View File

@@ -32,6 +32,7 @@ class DocType:
self.doclist = doclist self.doclist = doclist
def update_stock(self, args): def update_stock(self, args):
from stock.stock_ledger import update_entries_after
if not args.get("posting_date"): if not args.get("posting_date"):
posting_date = nowdate() posting_date = nowdate()
@@ -43,7 +44,12 @@ class DocType:
if args.get("actual_qty"): if args.get("actual_qty"):
# update valuation and qty after transaction for post dated entry # update valuation and qty after transaction for post dated entry
self.update_entries_after(args.get("posting_date"), args.get("posting_time")) update_entries_after({
item_code: self.doc.item_code,
warehouse: self.doc.warehouse,
posting_date: args.get("posting_date"),
posting_time: args.get("posting_time")
})
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # update the stock values (for current quantities)
@@ -69,233 +75,110 @@ class DocType:
""", (self.doc.item_code, self.doc.warehouse), as_dict=1) """, (self.doc.item_code, self.doc.warehouse), as_dict=1)
return sle and sle[0] or None return sle and sle[0] or None
def get_sle_prev_timebucket(self, posting_date = '1900-01-01', posting_time = '12:00'):
"""get previous stock ledger entry before current time-bucket"""
# get the last sle before the current time-bucket, so that all values
# are reposted from the current time-bucket onwards.
# this is necessary because at the time of cancellation, there may be
# entries between the cancelled entries in the same time-bucket
sle = sql("""
select * from `tabStock Ledger Entry`
where item_code = %s
and warehouse = %s
and ifnull(is_cancelled, 'No') = 'No'
and timestamp(posting_date, posting_time) < timestamp(%s, %s)
order by timestamp(posting_date, posting_time) desc, name desc
limit 1
""", (self.doc.item_code, self.doc.warehouse, posting_date, posting_time), as_dict=1)
return sle and sle[0] or {}
def validate_negative_stock(self, cqty, s):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
diff = cqty + s['actual_qty']
if diff < 0 and (abs(diff) > 0.0001) and s['is_cancelled'] == 'No':
self.exc_list.append({
"diff": diff,
"posting_date": s["posting_date"],
"posting_time": s["posting_time"],
"voucher_type": s["voucher_type"],
"voucher_no": s["voucher_no"]
})
return True
else:
return False
def get_serialized_inventory_values(self, val_rate, in_rate, opening_qty, \
actual_qty, is_cancelled, serial_nos):
"""
get serialized inventory values
"""
if flt(in_rate) < 0: # wrong incoming rate
in_rate = val_rate
elif flt(in_rate) == 0 or flt(actual_qty) < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
in_rate = flt(sql("""select ifnull(avg(purchase_rate), 0)
from `tabSerial No` where name in (%s)""" % (serial_nos))[0][0])
if in_rate and val_rate == 0: # First entry
val_rate = in_rate
# val_rate is same as previous entry if val_rate is negative
# Otherwise it will be calculated as per moving average
elif opening_qty + actual_qty > 0 and ((opening_qty * val_rate) + \
(actual_qty * in_rate)) > 0:
val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
(opening_qty + actual_qty)
return val_rate, in_rate
def get_moving_average_inventory_values(self, val_rate, in_rate, opening_qty, actual_qty, is_cancelled):
if flt(in_rate) == 0 or flt(actual_qty) < 0:
# In case of delivery/stock issue in_rate = 0 or wrong incoming rate
in_rate = val_rate
# val_rate is same as previous entry if :
# 1. actual qty is negative(delivery note / stock entry)
# 2. cancelled entry
# 3. val_rate is negative
# Otherwise it will be calculated as per moving average
if actual_qty > 0 and (opening_qty + actual_qty) > 0 and is_cancelled == 'No' \
and ((opening_qty * val_rate) + (actual_qty * in_rate)) > 0:
opening_qty = opening_qty > 0 and opening_qty or 0
val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
(opening_qty + actual_qty)
elif (opening_qty + actual_qty) <= 0:
val_rate = 0
return val_rate, in_rate
def get_fifo_inventory_values(self, in_rate, actual_qty):
# add batch to fcfs balance
if actual_qty > 0:
self.fcfs_bal.append([flt(actual_qty), flt(in_rate)])
# remove from fcfs balance
else:
incoming_cost = 0
withdraw = flt(abs(actual_qty))
while withdraw:
if not self.fcfs_bal:
break # nothing in store
batch = self.fcfs_bal[0]
if batch[0] <= withdraw:
# not enough or exactly same qty in current batch, clear batch
incoming_cost += flt(batch[1])*flt(batch[0])
withdraw -= batch[0]
self.fcfs_bal.pop(0)
else: # def get_serialized_inventory_values(self, val_rate, in_rate, opening_qty, \
# all from current batch # actual_qty, is_cancelled, serial_nos):
incoming_cost += flt(batch[1])*flt(withdraw) # """
batch[0] -= withdraw # get serialized inventory values
withdraw = 0 # """
# if flt(in_rate) < 0: # wrong incoming rate
# in_rate = val_rate
# elif flt(in_rate) == 0 or flt(actual_qty) < 0:
# # In case of delivery/stock issue, get average purchase rate
# # of serial nos of current entry
# in_rate = flt(sql("""select ifnull(avg(purchase_rate), 0)
# from `tabSerial No` where name in (%s)""" % (serial_nos))[0][0])
#
# if in_rate and val_rate == 0: # First entry
# val_rate = in_rate
# # val_rate is same as previous entry if val_rate is negative
# # Otherwise it will be calculated as per moving average
# elif opening_qty + actual_qty > 0 and ((opening_qty * val_rate) + \
# (actual_qty * in_rate)) > 0:
# val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
# (opening_qty + actual_qty)
# return val_rate, in_rate
#
# def get_moving_average_inventory_values(self, val_rate, in_rate, opening_qty, actual_qty, is_cancelled):
# if flt(in_rate) == 0 or flt(actual_qty) < 0:
# # In case of delivery/stock issue in_rate = 0 or wrong incoming rate
# in_rate = val_rate
#
# # val_rate is same as previous entry if :
# # 1. actual qty is negative(delivery note / stock entry)
# # 2. cancelled entry
# # 3. val_rate is negative
# # Otherwise it will be calculated as per moving average
# if actual_qty > 0 and (opening_qty + actual_qty) > 0 and is_cancelled == 'No' \
# and ((opening_qty * val_rate) + (actual_qty * in_rate)) > 0:
# opening_qty = opening_qty > 0 and opening_qty or 0
# val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
# (opening_qty + actual_qty)
# elif (opening_qty + actual_qty) <= 0:
# val_rate = 0
# return val_rate, in_rate
#
# def get_fifo_inventory_values(self, in_rate, actual_qty):
# # add batch to fcfs balance
# if actual_qty > 0:
# self.fcfs_bal.append([flt(actual_qty), flt(in_rate)])
#
# # remove from fcfs balance
# else:
# incoming_cost = 0
# withdraw = flt(abs(actual_qty))
# while withdraw:
# if not self.fcfs_bal:
# break # nothing in store
#
# batch = self.fcfs_bal[0]
#
# if batch[0] <= withdraw:
# # not enough or exactly same qty in current batch, clear batch
# incoming_cost += flt(batch[1])*flt(batch[0])
# withdraw -= batch[0]
# self.fcfs_bal.pop(0)
#
#
# else:
# # all from current batch
# incoming_cost += flt(batch[1])*flt(withdraw)
# batch[0] -= withdraw
# withdraw = 0
#
# in_rate = incoming_cost / flt(abs(actual_qty))
#
# fcfs_val = sum([flt(d[0])*flt(d[1]) for d in self.fcfs_bal])
# fcfs_qty = sum([flt(d[0]) for d in self.fcfs_bal])
# val_rate = fcfs_qty and fcfs_val / fcfs_qty or 0
#
# return val_rate, in_rate
#
# def get_valuation_rate(self, val_method, serial_nos, val_rate, in_rate, stock_val, cqty, s):
# if serial_nos:
# val_rate, in_rate = self.get_serialized_inventory_values( \
# val_rate, in_rate, opening_qty = cqty, actual_qty = s['actual_qty'], \
# is_cancelled = s['is_cancelled'], serial_nos = serial_nos)
# elif val_method == 'Moving Average':
# val_rate, in_rate = self.get_moving_average_inventory_values( \
# val_rate, in_rate, opening_qty = cqty, actual_qty = s['actual_qty'], \
# is_cancelled = s['is_cancelled'])
# elif val_method == 'FIFO':
# val_rate, in_rate = self.get_fifo_inventory_values(in_rate, \
# actual_qty = s['actual_qty'])
# return val_rate, in_rate
in_rate = incoming_cost / flt(abs(actual_qty)) # def get_stock_value(self, val_method, cqty, val_rate, serial_nos):
# if serial_nos:
fcfs_val = sum([flt(d[0])*flt(d[1]) for d in self.fcfs_bal]) # stock_val = flt(val_rate) * flt(cqty)
fcfs_qty = sum([flt(d[0]) for d in self.fcfs_bal]) # elif val_method == 'Moving Average':
val_rate = fcfs_qty and fcfs_val / fcfs_qty or 0 # stock_val = flt(cqty) > 0 and flt(val_rate) * flt(cqty) or 0
# elif val_method == 'FIFO':
return val_rate, in_rate # stock_val = sum([flt(d[0])*flt(d[1]) for d in self.fcfs_bal])
# return stock_val
def get_valuation_rate(self, val_method, serial_nos, val_rate, in_rate, stock_val, cqty, s):
if serial_nos:
val_rate, in_rate = self.get_serialized_inventory_values( \
val_rate, in_rate, opening_qty = cqty, actual_qty = s['actual_qty'], \
is_cancelled = s['is_cancelled'], serial_nos = serial_nos)
elif val_method == 'Moving Average':
val_rate, in_rate = self.get_moving_average_inventory_values( \
val_rate, in_rate, opening_qty = cqty, actual_qty = s['actual_qty'], \
is_cancelled = s['is_cancelled'])
elif val_method == 'FIFO':
val_rate, in_rate = self.get_fifo_inventory_values(in_rate, \
actual_qty = s['actual_qty'])
return val_rate, in_rate
def get_stock_value(self, val_method, cqty, val_rate, serial_nos):
if serial_nos:
stock_val = flt(val_rate) * flt(cqty)
elif val_method == 'Moving Average':
stock_val = flt(cqty) > 0 and flt(val_rate) * flt(cqty) or 0
elif val_method == 'FIFO':
stock_val = sum([flt(d[0])*flt(d[1]) for d in self.fcfs_bal])
return stock_val
def update_entries_after(self, posting_date, posting_time, verbose=1):
"""
update valution rate and qty after transaction
from the current time-bucket onwards
"""
# Get prev sle
prev_sle = self.get_sle_prev_timebucket(posting_date, posting_time)
# if no prev sle, start from the first one (for repost)
if not prev_sle:
cqty, cval, val_rate, stock_val, self.fcfs_bal = 0, 0, 0, 0, []
# normal
else:
cqty = flt(prev_sle.get('qty_after_transaction', 0))
cval =flt(prev_sle.get('stock_value', 0))
val_rate = flt(prev_sle.get('valuation_rate', 0))
self.fcfs_bal = eval(prev_sle.get('stock_queue', '[]') or '[]')
# get valuation method
from stock.utils import get_valuation_method
val_method = get_valuation_method(self.doc.item_code)
# allow negative stock (only for moving average method)
from webnotes.utils import get_defaults
allow_negative_stock = get_defaults().get('allow_negative_stock', 0)
# recalculate the balances for all stock ledger entries
# after the prev sle
sll = sql("""
select *
from `tabStock Ledger Entry`
where item_code = %s
and warehouse = %s
and ifnull(is_cancelled, 'No') = 'No'
and timestamp(posting_date, posting_time) > timestamp(%s, %s)
order by timestamp(posting_date, posting_time) asc, name asc""", \
(self.doc.item_code, self.doc.warehouse, \
prev_sle.get('posting_date','1900-01-01'), \
prev_sle.get('posting_time', '12:00')), as_dict = 1)
self.exc_list = []
for sle in sll:
# block if stock level goes negative on any date
if (val_method != 'Moving Average') or (cint(allow_negative_stock) == 0):
if self.validate_negative_stock(cqty, sle):
cqty += sle['actual_qty']
continue
stock_val, in_rate = 0, sle['incoming_rate'] # IN
serial_nos = sle["serial_no"] and ("'"+"', '".join(cstr(sle["serial_no"]).split('\n')) \
+ "'") or ''
# Get valuation rate
val_rate, in_rate = self.get_valuation_rate(val_method, serial_nos, \
val_rate, in_rate, stock_val, cqty, sle)
# Qty upto the sle
cqty += sle['actual_qty']
# Stock Value upto the sle
stock_val = self.get_stock_value(val_method, cqty, val_rate, serial_nos)
# update current sle
sql("""update `tabStock Ledger Entry`
set qty_after_transaction=%s, valuation_rate=%s, stock_queue=%s, stock_value=%s,
incoming_rate = %s where name=%s""", \
(cqty, flt(val_rate), cstr(self.fcfs_bal), stock_val, in_rate, sle['name']))
if self.exc_list:
deficiency = min(e["diff"] for e in self.exc_list)
msg = """Negative stock error:
Cannot complete this transaction because stock will start
becoming negative (%s) for Item <b>%s</b> in Warehouse
<b>%s</b> on <b>%s %s</b> in Transaction %s %s.
Total Quantity Deficiency: <b>%s</b>""" % \
(self.exc_list[0]["diff"], self.doc.item_code, self.doc.warehouse,
self.exc_list[0]["posting_date"], self.exc_list[0]["posting_time"],
self.exc_list[0]["voucher_type"], self.exc_list[0]["voucher_no"],
abs(deficiency))
if verbose:
msgprint(msg, raise_exception=1)
else:
raise webnotes.ValidationError, msg
# update the bin
if sll or not prev_sle:
sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value = %s,
projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty -
reserved_qty) where name=%s
""", (flt(val_rate), cqty, flt(stock_val), self.doc.name))
def reorder_item(self,doc_type,doc_name): def reorder_item(self,doc_type,doc_name):
""" Reorder item if stock reaches reorder level""" """ Reorder item if stock reaches reorder level"""

View File

@@ -219,6 +219,8 @@ class DocType:
def update_sle(self): def update_sle(self):
""" Recalculate valuation rate in all sle after pr posting date""" """ Recalculate valuation rate in all sle after pr posting date"""
from stock.stock_ledger import update_entries_after
for pr in self.selected_pr: for pr in self.selected_pr:
pr_obj = get_obj('Purchase Receipt', pr, with_children = 1) pr_obj = get_obj('Purchase Receipt', pr, with_children = 1)
@@ -229,11 +231,13 @@ class DocType:
self.update_serial_no(d.serial_no, d.valuation_rate) self.update_serial_no(d.serial_no, d.valuation_rate)
sql("update `tabStock Ledger Entry` set incoming_rate = '%s' where voucher_detail_no = '%s'"%(flt(d.valuation_rate), d.name)) sql("update `tabStock Ledger Entry` set incoming_rate = '%s' where voucher_detail_no = '%s'"%(flt(d.valuation_rate), d.name))
bin = sql("select t1.name, t2.posting_date, t2.posting_time from `tabBin` t1, `tabStock Ledger Entry` t2 where t2.voucher_detail_no = '%s' and t2.item_code = t1.item_code and t2.warehouse = t1.warehouse LIMIT 1" % d.name) res = sql("""select item_code, warehouse, posting_date, posting_time
from `tabStock Ledger Entry` where voucher_detail_no = %s LIMIT 1""",
d.name, as_dict=1)
# update valuation rate after pr posting date # update valuation rate after pr posting date
if bin and bin[0][0]: if res:
obj = get_obj('Bin', bin[0][0]).update_entries_after(bin[0][1], bin[0][2]) update_entries_after(res[0])
def update_serial_no(self, sr_no, rate): def update_serial_no(self, sr_no, rate):

View File

@@ -19,22 +19,17 @@ import webnotes
import json import json
from webnotes import msgprint, _ from webnotes import msgprint, _
from webnotes.utils import cstr, flt from webnotes.utils import cstr, flt
from webnotes.model.controller import DocListController
class DocType(DocListController):
class DocType:
def __init__(self, doc, doclist=[]):
self.doc = doc
self.doclist = doclist
def validate(self): def validate(self):
self.validate_data() self.validate_data()
def on_submit(self): def on_submit(self):
self.create_stock_ledger_entries() self.insert_stock_ledger_entries()
def on_cancel(self): def on_cancel(self):
pass self.delete_stock_ledger_entries()
def validate_data(self): def validate_data(self):
data = json.loads(self.doc.reconciliation_json) data = json.loads(self.doc.reconciliation_json)
@@ -105,96 +100,116 @@ class DocType:
except Exception, e: except Exception, e:
self.validation_messages.append(_("Row # ") + ("%d: " % (row_num+2)) + cstr(e)) self.validation_messages.append(_("Row # ") + ("%d: " % (row_num+2)) + cstr(e))
def create_stock_ledger_entries(self): def insert_stock_ledger_entries(self):
""" find difference between current and expected entries """ find difference between current and expected entries
and create stock ledger entries based on the difference""" and create stock ledger entries based on the difference"""
from stock.utils import get_previous_sle, get_valuation_method from stock.utils import get_previous_sle, get_valuation_method
def _qty_diff(qty, previous_sle):
return qty != "" and (flt(qty) - flt(previous_sle.get("qty_after_transaction"))) or 0.0
def _rate_diff(rate, previous_sle):
return rate != "" and (flt(rate) - flt(previous_sle.get("valuation_rate"))) or 0.0
def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate):
if previous_valuation_rate == 0:
return valuation_rate
else:
return (qty * valuation_rate - previous_qty * previous_valuation_rate) \
/ flt(qty - previous_qty)
row_template = ["item_code", "warehouse", "qty", "valuation_rate"] row_template = ["item_code", "warehouse", "qty", "valuation_rate"]
data = json.loads(self.doc.reconciliation_json) data = json.loads(self.doc.reconciliation_json)
for row_num, row in enumerate(data[1:]): for row_num, row in enumerate(data[1:]):
row = webnotes._dict(zip(row_template, row)) row = webnotes._dict(zip(row_template, row))
args = webnotes._dict({ previous_sle = get_previous_sle({
"__islocal": 1,
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse, "warehouse": row.warehouse,
"posting_date": self.doc.posting_date, "posting_date": self.doc.posting_date,
"posting_time": self.doc.posting_time, "posting_time": self.doc.posting_time
"voucher_type": self.doc.doctype,
"voucher_no": self.doc.name,
"company": webnotes.conn.get_default("company")
}) })
previous_sle = get_previous_sle(args)
qty_diff = _qty_diff(row.qty, previous_sle)
if get_valuation_method(row.item_code) == "Moving Average": if get_valuation_method(row.item_code) == "Moving Average":
if qty_diff: self.sle_for_moving_avg(row, previous_sle)
incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate),
flt(previous_sle.qty_after_transaction),
flt(previous_sle.valuation_rate))
# create sle
webnotes.model_wrapper([args.update({
"actual_qty": qty_diff,
"incoming_rate": incoming_rate
})]).save()
elif _rate_diff(row.valuation_rate, previous_sle) and \
previous_sle.qty_after_transaction >= 0:
# make +1, -1 entry
incoming_rate = _get_incoming_rate(flt(previous_sle.qty_after_transaction) + 1,
flt(row.valuation_rate), flt(previous_sle.qty_after_transaction),
flt(previous_sle.valuation_rate))
# +1 entry
webnotes.model_wrapper([args.copy().update({
"actual_qty": 1,
"incoming_rate": incoming_rate
})]).save()
# -1 entry
webnotes.model_wrapper([args.update({"actual_qty": -1})]).save()
# else:
# # show message that stock is negative, hence can't update valuation
else: else:
# FIFO self.sle_for_fifo(row, previous_sle)
previous_stock_queue = json.loads(previous_sle.stock_queue)
if previous_stock_queue != [[row.qty, row.valuation_rate]]: def sle_for_moving_avg(self, row, previous_sle):
# make entry as per attachment """Insert Stock Ledger Entries for Moving Average valuation"""
sle_wrapper = webnotes.model_wrapper([args.copy().update({ def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate):
"actual_qty": row.qty, if previous_valuation_rate == 0:
"incoming_rate": row.valuation_rate return valuation_rate
})]) else:
sle_wrapper.save() return (qty * valuation_rate - previous_qty * previous_valuation_rate) \
/ flt(qty - previous_qty)
# Make reverse entry change_in_qty = row.qty != "" and \
qty = sum((flt(fifo_item[0]) for fifo_item in previous_stock_queue)) (flt(row.qty) != flt(previous_sle.get("qty_after_transaction")))
webnotes.model_wrapper([args.update({"actual_qty": -1 * qty})]).save()
change_in_rate = row.valuation_rate != "" and \
(flt(row.valuation_rate) != flt(previous_sle.get("valuation_rate")))
if change_in_qty:
incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate),
flt(previous_sle.qty_after_transaction),
flt(previous_sle.valuation_rate))
self.insert_entries({"actual_qty": qty_diff, "incoming_rate": incoming_rate}, row)
elif change_in_rate and previous_sle.qty_after_transaction >= 0:
incoming_rate = _get_incoming_rate(flt(previous_sle.qty_after_transaction) + 1,
flt(row.valuation_rate), flt(previous_sle.qty_after_transaction),
flt(previous_sle.valuation_rate))
# +1 entry
self.insert_entries({"actual_qty": 1, "incoming_rate": incoming_rate}, row)
# -1 entry
self.insert_entries({"actual_qty": -1}, row)
def sle_for_fifo(self, row, previous_sle):
"""Insert Stock Ledger Entries for FIFO valuation"""
previous_stock_queue = json.loads(previous_sle.stock_queue)
if previous_stock_queue != [[row.qty, row.valuation_rate]]:
# make entry as per attachment
self.insert_entries({"actual_qty": row.qty, "incoming_rate": row.valuation_rate},
row)
# Make reverse entry
qty = sum((flt(fifo_item[0]) for fifo_item in previous_stock_queue))
self.insert_entries({"actual_qty": -1 * qty}, row)
def insert_entries(self, opts, row):
"""Insert Stock Ledger Entries"""
args = {
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.doc.posting_date,
"posting_time": self.doc.posting_time,
"voucher_type": self.doc.doctype,
"voucher_no": self.doc.name,
"company": webnotes.conn.get_default("company"),
"is_cancelled": "No"
}
args.update(opts)
return webnotes.model_wrapper([args]).insert()
def delete_stock_ledger_entries(self):
""" Delete Stock Ledger Entries related to this Stock Reconciliation
and repost future Stock Ledger Entries"""
from stock.stock_ledger import update_entries_after
existing_entries = webnotes.conn.sql("""select item_code, warehouse
from `tabStock Ledger Entry` where voucher_type='Stock Reconciliation'
and voucher_no=%s""", self.doc.name, as_dict=1)
# delete entries
webnotes.conn.sql("""delete from `tabStock Ledger Entry`
where voucher_type='Stock Reconciliation' and voucher_no=%s""", self.doc.name)
# repost future entries for selected item_code, warehouse
for entries in existing_entries:
update_entries_after({
item_code: entries.item_code,
warehouse: entries.warehouse,
posting_date: self.doc.posting_date,
posting_time: self.doc.posting_time
})
@webnotes.whitelist() @webnotes.whitelist()

View File

@@ -77,6 +77,8 @@ class DocType:
def update_stock_ledger_entry(self): def update_stock_ledger_entry(self):
# update stock ledger entry # update stock ledger entry
from stock.stock_ledger import update_entries_after
if flt(self.doc.conversion_factor) != flt(1): if flt(self.doc.conversion_factor) != flt(1):
sql("update `tabStock Ledger Entry` set stock_uom = '%s', actual_qty = ifnull(actual_qty,0) * '%s' where item_code = '%s' " % (self.doc.new_stock_uom, self.doc.conversion_factor, self.doc.item_code)) sql("update `tabStock Ledger Entry` set stock_uom = '%s', actual_qty = ifnull(actual_qty,0) * '%s' where item_code = '%s' " % (self.doc.new_stock_uom, self.doc.conversion_factor, self.doc.item_code))
else: else:
@@ -89,9 +91,7 @@ class DocType:
if flt(self.doc.conversion_factor) != flt(1): if flt(self.doc.conversion_factor) != flt(1):
wh = sql("select name from `tabWarehouse`") wh = sql("select name from `tabWarehouse`")
for w in wh: for w in wh:
bin = sql("select name from `tabBin` where item_code = '%s' and warehouse = '%s'" % (self.doc.item_code, w[0])) update_entries_after({item_code: self.doc.item_code, warehouse: w[0]})
if bin and bin[0][0]:
get_obj("Bin", bin[0][0]).update_entries_after(posting_date = '', posting_time = '')
# acknowledge user # acknowledge user
msgprint("Item Valuation Updated Successfully.") msgprint("Item Valuation Updated Successfully.")

View File

@@ -104,8 +104,9 @@ class DocType:
def repost(self, item_code, warehouse=None): def repost(self, item_code, warehouse=None):
self.repost_actual_qty(item_code, warehouse)
bin = self.get_bin(item_code, warehouse) bin = self.get_bin(item_code, warehouse)
self.repost_actual_qty(bin)
self.repost_reserved_qty(bin) self.repost_reserved_qty(bin)
self.repost_indented_qty(bin) self.repost_indented_qty(bin)
self.repost_ordered_qty(bin) self.repost_ordered_qty(bin)
@@ -115,8 +116,17 @@ class DocType:
bin.doc.save() bin.doc.save()
def repost_actual_qty(self, bin): def repost_actual_qty(self, item_code, warehouse=None):
bin.update_entries_after(posting_date = '0000-00-00', posting_time = '00:00') from stock.stock_ledger import update_entries_after
if not warehouse:
warehouse = self.doc.name
update_entries_after({
item_code: item_code,
warehouse: warehouse,
posting_date: '1900-01-01',
posting_time = '10:00'
})
def repost_reserved_qty(self, bin): def repost_reserved_qty(self, bin):
reserved_qty = webnotes.conn.sql(""" reserved_qty = webnotes.conn.sql("""

252
stock/stock_ledger.py Normal file
View File

@@ -0,0 +1,252 @@
# ERPNext - web based ERP (http://erpnext.com)
# Copyright (C) 2012 Web Notes Technologies Pvt Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import webnotes
from webnotes import msgprint, _
from webnotes.utils import cint
from stock.utils import _msgprint, get_valuation_method
# future reposting
_exceptions = []
def update_entries_after(args, verbose=1):
"""
update valution rate and qty after transaction
from the current time-bucket onwards
args = {
"item_code": "ABC",
"warehouse": "XYZ",
"posting_date": "2012-12-12",
"posting_time": "12:00"
}
"""
previous_sle = get_sle_before_datetime(args)
qty_after_transaction = flt(previous_sle.get("qty_after_transaction"))
valuation_rate = flt(previous_sle.get("valuation_rate"))
stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
entries_to_fix = get_sle_after_datetime(previous_sle or \
{"item_code": args["item_code"], "warehouse": args["warehouse"]})
valuation_method = get_valuation_method(args["item_code"])
for sle in entries_to_fix:
if sle.serial_nos or valuation_method == "FIFO" or \
not cint(webnotes.conn.get_default("allow_negative_stock")):
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
if not validate_negative_stock(qty_after_transaction, sle):
qty_after_transaction += flt(sle.actual_qty)
continue
if sle.serial_nos:
valuation_rate, incoming_rate = get_serialized_values(qty_after_transaction, sle,
valuation_rate)
elif valuation_method == "Moving Average":
valuation_rate, incoming_rate = get_moving_average_values(qty_after_transaction, sle,
valuation_rate)
else:
valuation_rate, incoming_rate = get_fifo_values(qty_after_transaction, sle,
stock_queue)
qty_after_transaction += flt(sle.actual_qty)
# get stock value
if serial_nos:
stock_value = qty_after_transaction * valuation_rate
elif valuation_method == "Moving Average":
stock_value = (qty_after_transaction > 0) and \
(qty_after_transaction * valuation_rate) or 0
else:
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
# update current sle
webnotes.conn.sql("""update `tabStock Ledger Entry`
set qty_after_transaction=%s, valuation_rate=%s, stock_queue=%s, stock_value=%s,
incoming_rate = %s where name=%s""", (qty_after_transaction, valuation_rate,
json.dumps(stock_queue), stock_value, incoming_rate, sle.name))
if _exceptions:
_raise_exceptions(args)
# update bin
webnotes.conn.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value=%s,
projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty - reserved_qty)
where item_code=%s and warehouse=%s""", (valuation_rate, qty_after_transaction,
stock_value, args["item_code"], args["warehouse"]))
def get_sle_before_datetime(args):
"""
get previous stock ledger entry before current time-bucket
Details:
get the last sle before the current time-bucket, so that all values
are reposted from the current time-bucket onwards.
this is necessary because at the time of cancellation, there may be
entries between the cancelled entries in the same time-bucket
"""
sle = get_stock_ledger_entries(args,
["timestamp(posting_date, posting_time) < timestamp(%%(posting_date)s, %%(posting_time)s)"],
"limit 1")
return sle and sle[0] or webnotes._dict()
def get_sle_after_datetime(args):
"""get Stock Ledger Entries after a particular datetime, for reposting"""
return get_stock_ledger_entries(args,
["timestamp(posting_date, posting_time) > timestamp(%%(posting_date)s, %%(posting_time)s)"])
def get_stock_ledger_entries(args, conditions=None, limit=None):
"""get stock ledger entries filtered by specific posting datetime conditions"""
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "12:00"
return webnotes.conn.sql("""select * from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and warehouse = %%(warehouse)s
and ifnull(is_cancelled, 'No') = 'No'
%(conditions)s
order by timestamp(posting_date, posting_time) desc, name desc
%(limit)s""" % {
"conditions": conditions and ("and " + " and ".join(conditions)) or "",
"limit": limit or ""
}, args, as_dict=1)
def validate_negative_stock(qty_after_transaction, sle):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
diff = qty_after_transaction + flt(sle.actual_qty)
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
global _exceptions
exc = sle.copy().update({"diff": diff})
_exceptions.append(exc)
return False
else:
return True
def get_serialized_values(qty_after_transaction, sle, valuation_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
serial_nos = cstr(sle.serial_nos).split("\n")
if incoming_rate < 0:
# wrong incoming rate
incoming_rate = valuation_rate
elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
incoming_rate = flt(webnotes.conn.sql("""select avg(ifnull(purchase_rate, 0))
from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_nos))),
tuple(serial_nos))[0][0])
if incoming_rate and not valuation_rate:
valuation_rate = incoming_rate
else:
new_stock_qty = qty_after_transaction + actual_qty
if new_stock_qty > 0:
new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
if new_stock_value > 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
valuation_rate = new_stock_value / new_stock_qty
return valuation_rate, incoming_rate
def get_moving_average_values(qty_after_transaction, sle, valuation_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if not incoming_rate or actual_qty < 0:
# In case of delivery/stock issue in_rate = 0 or wrong incoming rate
incoming_rate = valuation_rate
# val_rate is same as previous entry if :
# 1. actual qty is negative(delivery note / stock entry)
# 2. cancelled entry
# 3. val_rate is negative
# Otherwise it will be calculated as per moving average
new_stock_qty = qty_after_transaction + actual_qty
new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
if actual_qty > 0 and new_stock_qty > 0 and new_stock_value > 0:
valuation_rate = new_stock_value / flt(new_stock_qty)
elif new_stock_qty <= 0:
valuation_rate = 0.0
return valuation_rate, incoming_rate
def get_fifo_values(qty_after_transaction, sle, stock_queue):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if not stock_queue:
stock_queue.append([0, 0])
if actual_qty > 0:
if stock_queue[-1][0] > 0:
stock_queue.append([actual_qty, incoming_rate])
else:
qty = stock_queue[-1][0] + actual_qty
stock_queue[-1] = [qty, qty > 0 and incoming_rate or 0]
else:
incoming_cost = 0
qty_to_pop = abs(actual_qty)
while qty_to_pop:
batch = stock_queue[0]
if 0 < batch[0] <= qty_to_pop:
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
incoming_cost += flt(batch[0]) * flt(batch[1])
qty_to_pop -= batch[0]
stock_queue.pop(0)
else:
# all from current batch
incoming_cost += flt(qty_to_pop) * flt(batch[1])
batch[0] -= qty_to_pop
qty_to_pop = 0
incoming_rate = incoming_cost / flt(abs(actual_qty))
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
stock_qty = sum((flt(batch[0]) for batch in stock_queue))
valuation_rate = stock_qty and (stock_value / flt(stock_qty)) or 0
return valuation_rate, incoming_rate
def _raise_exceptions(args):
deficiency = min(e["diff"] for e in _exceptions)
msg = """Negative stock error:
Cannot complete this transaction because stock will start
becoming negative (%s) for Item <b>%s</b> in Warehouse
<b>%s</b> on <b>%s %s</b> in Transaction %s %s.
Total Quantity Deficiency: <b>%s</b>""" % \
(_exceptions[0]["diff"], args.get("item_code"), args.get("warehouse"),
_exceptions[0]["posting_date"], _exceptions[0]["posting_time"],
_exceptions[0]["voucher_type"], _exceptions[0]["voucher_no"],
abs(deficiency))
if verbose:
msgprint(msg, raise_exception=1)
else:
raise webnotes.ValidationError, msg

View File

@@ -68,6 +68,14 @@ def get_previous_sle(args):
get the last sle on or before the current time-bucket, get the last sle on or before the current time-bucket,
to get actual qty before transaction, this function to get actual qty before transaction, this function
is called from various transaction like stock entry, reco etc is called from various transaction like stock entry, reco etc
args = {
"item_code": "ABC",
"warehouse": "XYZ",
"posting_date": "2012-12-12",
"posting_time": "12:00",
"sle": "name of reference Stock Ledger Entry"
}
""" """
if not args.get("posting_date"): if not args.get("posting_date"):
args["posting_date"] = "1900-01-01" args["posting_date"] = "1900-01-01"