diff --git a/patches/april_2012/repost_stock_for_posting_time.py b/patches/april_2012/repost_stock_for_posting_time.py index a1283a0327d..2249ac190f6 100644 --- a/patches/april_2012/repost_stock_for_posting_time.py +++ b/patches/april_2012/repost_stock_for_posting_time.py @@ -1,10 +1,15 @@ from __future__ import unicode_literals def execute(): import webnotes - from webnotes.model.code import get_obj - - 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") + 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) 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: - get_obj('Bin', d[0]).update_entries_after(posting_date = '2000-01-01', posting_time = '12:01') + from stock.stock_ledger import update_entries_after + for d in res: + update_entries_after({ + item_code: d.item_code, + warehouse: d.warehouse, + posting_date: '2000-01-01', + posting_time: '12:01' + }) diff --git a/patches/july_2012/repost_stock_due_to_wrong_packing_list.py b/patches/july_2012/repost_stock_due_to_wrong_packing_list.py index b20290287c3..10d4f500b97 100644 --- a/patches/july_2012/repost_stock_due_to_wrong_packing_list.py +++ b/patches/july_2012/repost_stock_due_to_wrong_packing_list.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import webnotes +from stock.stock_ledger import update_entries_after def execute(): # add index @@ -80,8 +81,13 @@ def cleanup_wrong_sle(): for d in sle: webnotes.conn.sql("update `tabStock Ledger Entry` set is_cancelled = 'Yes' where name = %s", 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): from webnotes.model.doc import Document cmt = Document('Comment') @@ -91,11 +97,4 @@ def create_comment(dn): cmt.comment_doctype = 'Stock Ledger Entry' cmt.comment_docname = dn 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') + \ No newline at end of file diff --git a/patches/september_2012/repost_stock.py b/patches/september_2012/repost_stock.py index c6b6ce39f77..1fec9499ab5 100644 --- a/patches/september_2012/repost_stock.py +++ b/patches/september_2012/repost_stock.py @@ -17,12 +17,17 @@ from __future__ import unicode_literals def execute(): import webnotes - from webnotes.model.code import get_obj - bin = webnotes.conn.sql("select name from `tabBin`") + from stock.stock_ledger import update_entries_after + res = webnotes.conn.sql("select distinct item_code, warehouse from `tabStock Ledger Entry`") i=0 - for d in bin: + for d in res: 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: pass i += 1 diff --git a/stock/doctype/bin/bin.py b/stock/doctype/bin/bin.py index ea486cea0cc..c9bd9276491 100644 --- a/stock/doctype/bin/bin.py +++ b/stock/doctype/bin/bin.py @@ -32,6 +32,7 @@ class DocType: self.doclist = doclist def update_stock(self, args): + from stock.stock_ledger import update_entries_after if not args.get("posting_date"): posting_date = nowdate() @@ -43,8 +44,13 @@ class DocType: if args.get("actual_qty"): # 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): # update the stock values (for current quantities) self.doc.actual_qty = flt(self.doc.actual_qty) + flt(args.get("actual_qty", 0)) @@ -69,233 +75,110 @@ class DocType: """, (self.doc.item_code, self.doc.warehouse), as_dict=1) 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]) + # 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: + # # 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 - 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_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 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 - - 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 %s in Warehouse - %s on %s %s in Transaction %s %s. - Total Quantity Deficiency: %s""" % \ - (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): """ Reorder item if stock reaches reorder level""" diff --git a/stock/doctype/landed_cost_wizard/landed_cost_wizard.py b/stock/doctype/landed_cost_wizard/landed_cost_wizard.py index d5abb845961..7067e52d0bf 100644 --- a/stock/doctype/landed_cost_wizard/landed_cost_wizard.py +++ b/stock/doctype/landed_cost_wizard/landed_cost_wizard.py @@ -219,6 +219,8 @@ class DocType: def update_sle(self): """ Recalculate valuation rate in all sle after pr posting date""" + from stock.stock_ledger import update_entries_after + for pr in self.selected_pr: 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) 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 - if bin and bin[0][0]: - obj = get_obj('Bin', bin[0][0]).update_entries_after(bin[0][1], bin[0][2]) + if res: + update_entries_after(res[0]) def update_serial_no(self, sr_no, rate): diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 02a75a8dfbf..b82a7f0dc17 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -19,22 +19,17 @@ import webnotes import json from webnotes import msgprint, _ from webnotes.utils import cstr, flt +from webnotes.model.controller import DocListController - - -class DocType: - def __init__(self, doc, doclist=[]): - self.doc = doc - self.doclist = doclist - +class DocType(DocListController): def validate(self): self.validate_data() def on_submit(self): - self.create_stock_ledger_entries() + self.insert_stock_ledger_entries() def on_cancel(self): - pass + self.delete_stock_ledger_entries() def validate_data(self): data = json.loads(self.doc.reconciliation_json) @@ -105,23 +100,10 @@ class DocType: except Exception, 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 and create stock ledger entries based on the difference""" 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"] @@ -129,73 +111,106 @@ class DocType: for row_num, row in enumerate(data[1:]): row = webnotes._dict(zip(row_template, row)) - args = webnotes._dict({ - "__islocal": 1, + previous_sle = get_previous_sle({ "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") + "posting_time": self.doc.posting_time }) - previous_sle = get_previous_sle(args) - qty_diff = _qty_diff(row.qty, previous_sle) - if get_valuation_method(row.item_code) == "Moving Average": - if qty_diff: - 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 + self.sle_for_moving_avg(row, previous_sle) else: - # FIFO - previous_stock_queue = json.loads(previous_sle.stock_queue) - - if previous_stock_queue != [[row.qty, row.valuation_rate]]: - # make entry as per attachment - sle_wrapper = webnotes.model_wrapper([args.copy().update({ - "actual_qty": row.qty, - "incoming_rate": row.valuation_rate - })]) - sle_wrapper.save() - - # Make reverse entry - qty = sum((flt(fifo_item[0]) for fifo_item in previous_stock_queue)) - webnotes.model_wrapper([args.update({"actual_qty": -1 * qty})]).save() - - - - + self.sle_for_fifo(row, previous_sle) + + def sle_for_moving_avg(self, row, previous_sle): + """Insert Stock Ledger Entries for Moving Average valuation""" + 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) + change_in_qty = row.qty != "" and \ + (flt(row.qty) != flt(previous_sle.get("qty_after_transaction"))) + 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() def upload(): diff --git a/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py b/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py index 0af37d6e9cf..209bda5ff3b 100644 --- a/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py +++ b/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py @@ -77,6 +77,8 @@ class DocType: def update_stock_ledger_entry(self): # update stock ledger entry + from stock.stock_ledger import update_entries_after + 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)) else: @@ -89,9 +91,7 @@ class DocType: if flt(self.doc.conversion_factor) != flt(1): wh = sql("select name from `tabWarehouse`") for w in wh: - bin = sql("select name from `tabBin` where item_code = '%s' and warehouse = '%s'" % (self.doc.item_code, w[0])) - if bin and bin[0][0]: - get_obj("Bin", bin[0][0]).update_entries_after(posting_date = '', posting_time = '') + update_entries_after({item_code: self.doc.item_code, warehouse: w[0]}) # acknowledge user msgprint("Item Valuation Updated Successfully.") diff --git a/stock/doctype/warehouse/warehouse.py b/stock/doctype/warehouse/warehouse.py index 38ba3879d32..e65004bfe0a 100644 --- a/stock/doctype/warehouse/warehouse.py +++ b/stock/doctype/warehouse/warehouse.py @@ -104,8 +104,9 @@ class DocType: def repost(self, item_code, warehouse=None): + self.repost_actual_qty(item_code, warehouse) + bin = self.get_bin(item_code, warehouse) - self.repost_actual_qty(bin) self.repost_reserved_qty(bin) self.repost_indented_qty(bin) self.repost_ordered_qty(bin) @@ -115,8 +116,17 @@ class DocType: bin.doc.save() - def repost_actual_qty(self, bin): - bin.update_entries_after(posting_date = '0000-00-00', posting_time = '00:00') + def repost_actual_qty(self, item_code, warehouse=None): + 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): reserved_qty = webnotes.conn.sql(""" diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py new file mode 100644 index 00000000000..d4475efa3a6 --- /dev/null +++ b/stock/stock_ledger.py @@ -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 . + +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 %s in Warehouse + %s on %s %s in Transaction %s %s. + Total Quantity Deficiency: %s""" % \ + (_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 \ No newline at end of file diff --git a/stock/utils.py b/stock/utils.py index 28919c97fd4..b3bf26af136 100644 --- a/stock/utils.py +++ b/stock/utils.py @@ -68,6 +68,14 @@ def get_previous_sle(args): get the last sle on or before the current time-bucket, to get actual qty before transaction, this function 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"): args["posting_date"] = "1900-01-01"