From 9d0f636c46fa22da8103a07172bffa25955cf0be Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 7 Jan 2013 18:51:11 +0530 Subject: [PATCH 01/18] stock reconciliation - rewrite in progress --- manufacturing/doctype/bom/bom.py | 14 +- patches/january_2013/stock_reconciliation.py | 14 + .../installation_note/installation_note.py | 9 +- stock/doctype/bin/bin.py | 29 +- stock/doctype/stock_entry/stock_entry.py | 70 ++-- stock/doctype/stock_ledger/stock_ledger.py | 33 +- .../stock_ledger_entry/stock_ledger_entry.py | 10 +- .../stock_ledger_entry/stock_ledger_entry.txt | 223 ++++------- .../old_stock_reconciliation.py | 264 +++++++++++++ .../stock_reconciliation.js | 75 +++- .../stock_reconciliation.py | 369 +++++++----------- .../stock_reconciliation.txt | 205 +++++----- .../test_stock_reconciliation.py | 125 ++++++ stock/doctype/valuation_control/__init__.py | 1 - .../valuation_control/valuation_control.py | 131 ------- .../valuation_control/valuation_control.txt | 24 -- stock/report/stock_ledger/stock_ledger.txt | 2 +- stock/utils.py | 157 ++++++++ 18 files changed, 1002 insertions(+), 753 deletions(-) create mode 100644 patches/january_2013/stock_reconciliation.py create mode 100644 stock/doctype/stock_reconciliation/old_stock_reconciliation.py create mode 100644 stock/doctype/stock_reconciliation/test_stock_reconciliation.py delete mode 100644 stock/doctype/valuation_control/__init__.py delete mode 100644 stock/doctype/valuation_control/valuation_control.py delete mode 100644 stock/doctype/valuation_control/valuation_control.txt create mode 100644 stock/utils.py diff --git a/manufacturing/doctype/bom/bom.py b/manufacturing/doctype/bom/bom.py index bc80bfe33b..b4a51cd6e7 100644 --- a/manufacturing/doctype/bom/bom.py +++ b/manufacturing/doctype/bom/bom.py @@ -135,18 +135,24 @@ class DocType: where is_active = 1 and name = %s""", bom_no, as_dict=1) return bom and bom[0]['unit_cost'] or 0 - def get_valuation_rate(self, arg): + def get_valuation_rate(self, args): """ Get average valuation rate of relevant warehouses as per valuation method (MAR/FIFO) as on costing date """ + from stock.utils import get_incoming_rate dt = self.doc.costing_date or nowdate() time = self.doc.costing_date == nowdate() and now().split()[1] or '23:59' - warehouse = sql("select warehouse from `tabBin` where item_code = %s", arg['item_code']) + warehouse = sql("select warehouse from `tabBin` where item_code = %s", args['item_code']) rate = [] for wh in warehouse: - r = get_obj('Valuation Control').get_incoming_rate(dt, time, - arg['item_code'], wh[0], qty=arg.get('qty', 0)) + r = get_incoming_rate({ + item_code: args.get("item_code"), + warehouse: wh[0], + posting_date: dt, + posting_time: time, + qty: args.get("qty") or 0 + }) if r: rate.append(r) diff --git a/patches/january_2013/stock_reconciliation.py b/patches/january_2013/stock_reconciliation.py new file mode 100644 index 0000000000..e14044f7f0 --- /dev/null +++ b/patches/january_2013/stock_reconciliation.py @@ -0,0 +1,14 @@ +import webnotes + +def execute(): + rename_fields() + +def rename_fields(): + webnotes.reload_doc("stock", "doctype", "stock_ledger_entry") + + args = [["Stock Ledger Entry", "bin_aqat", "qty_after_transaction"], + ["Stock Ledger Entry", "fcfs_stack", "stock_queue"]] + for doctype, old_fieldname, new_fieldname in args: + webnotes.conn.sql("""update `tab%s` set `%s`=`%s`""" % + (doctype, new_fieldname, old_fieldname)) + \ No newline at end of file diff --git a/selling/doctype/installation_note/installation_note.py b/selling/doctype/installation_note/installation_note.py index 5a997fdc04..00d365afc7 100644 --- a/selling/doctype/installation_note/installation_note.py +++ b/selling/doctype/installation_note/installation_note.py @@ -23,6 +23,7 @@ from webnotes.model.doc import make_autoname from webnotes.model.wrapper import getlist, copy_doclist from webnotes.model.code import get_obj from webnotes import msgprint +from stock.utils import get_valid_serial_nos sql = webnotes.conn.sql @@ -117,10 +118,8 @@ class DocType(TransactionBase): #get list of serial no from previous_doc #---------------------------------------------- def get_prevdoc_serial_no(self, prevdoc_detail_docname, prevdoc_docname): - from stock.doctype.stock_ledger.stock_ledger import get_sr_no_list - res = sql("select serial_no from `tabDelivery Note Item` where name = '%s' and parent ='%s'" % (prevdoc_detail_docname, prevdoc_docname)) - return get_sr_no_list(res[0][0]) + return get_valid_serial_nos(res[0][0]) #check if all serial nos from current record exist in resp delivery note #--------------------------------------------------------------------------------- @@ -134,14 +133,12 @@ class DocType(TransactionBase): #---------------------------------------- def validate_serial_no(self): cur_s_no, prevdoc_s_no, sr_list = [], [], [] - from stock.doctype.stock_ledger.stock_ledger import get_sr_no_list - for d in getlist(self.doclist, 'installed_item_details'): self.is_serial_no_added(d.item_code, d.serial_no) if d.serial_no: - sr_list = get_sr_no_list(d.serial_no, d.qty, d.item_code) + sr_list = get_valid_serial_nos(d.serial_no, d.qty, d.item_code) self.is_serial_no_exist(d.item_code, sr_list) prevdoc_s_no = self.get_prevdoc_serial_no(d.prevdoc_detail_docname, d.prevdoc_docname) diff --git a/stock/doctype/bin/bin.py b/stock/doctype/bin/bin.py index d472e5fc4d..ea486cea0c 100644 --- a/stock/doctype/bin/bin.py +++ b/stock/doctype/bin/bin.py @@ -69,26 +69,6 @@ class DocType: """, (self.doc.item_code, self.doc.warehouse), as_dict=1) return sle and sle[0] or None - def get_prev_sle(self, posting_date = '1900-01-01', posting_time = '12:00', sle_id = ''): - """ - 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 - """ - - sle = sql(""" - select * from `tabStock Ledger Entry` - where item_code = %s - and warehouse = %s - and ifnull(is_cancelled, 'No') = 'No' - and name != %s - 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, sle_id, posting_date, posting_time), as_dict=1) - - return sle and sle[0] or {} - 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 @@ -242,13 +222,14 @@ class DocType: # normal else: - cqty = flt(prev_sle.get('bin_aqat', 0)) + 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('fcfs_stack', '[]') or '[]') + self.fcfs_bal = eval(prev_sle.get('stock_queue', '[]') or '[]') # get valuation method - val_method = get_obj('Valuation Control').get_valuation_method(self.doc.item_code) + 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 @@ -289,7 +270,7 @@ class DocType: stock_val = self.get_stock_value(val_method, cqty, val_rate, serial_nos) # update current sle sql("""update `tabStock Ledger Entry` - set bin_aqat=%s, valuation_rate=%s, fcfs_stack=%s, stock_value=%s, + 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'])) diff --git a/stock/doctype/stock_entry/stock_entry.py b/stock/doctype/stock_entry/stock_entry.py index 18440329ae..ea44f1bbf0 100644 --- a/stock/doctype/stock_entry/stock_entry.py +++ b/stock/doctype/stock_entry/stock_entry.py @@ -23,6 +23,7 @@ from webnotes.model.doc import Document, addchild from webnotes.model.wrapper import getlist, copy_doclist from webnotes.model.code import get_obj from webnotes import msgprint, _ +from stock.utils import get_previous_sle, get_incoming_rate sql = webnotes.conn.sql @@ -129,8 +130,6 @@ class DocType(TransactionBase): msgprint(_("Source and Target Warehouse cannot be same"), raise_exception=1) - - def validate_production_order(self, pro_obj=None): if not pro_obj: if self.doc.production_order: @@ -157,42 +156,24 @@ class DocType(TransactionBase): def get_stock_and_rate(self): """get stock and incoming rate on posting date""" for d in getlist(self.doclist, 'mtn_details'): + args = { + item_code: d.item_code, + warehouse: d.s_warehouse or d.t_warehouse, + posting_date: self.doc.posting_date, + posting_time: self.doc.posting_time, + qty: d.transfer_qty, + serial_no: d.serial_no, + bom_no: d.bom_no + + } # get actual stock at source warehouse - d.actual_qty = self.get_as_on_stock(d.item_code, d.s_warehouse or d.t_warehouse, - self.doc.posting_date, self.doc.posting_time) - + d.actual_qty = get_previous_sle(args).get("qty_after_transaction") or 0 + # get incoming rate if not flt(d.incoming_rate): - d.incoming_rate = self.get_incoming_rate(d.item_code, - d.s_warehouse or d.t_warehouse, self.doc.posting_date, - self.doc.posting_time, d.transfer_qty, d.serial_no, d.bom_no) - + d.incoming_rate = get_incoming_rate(args) + d.amount = flt(d.qty) * flt(d.incoming_rate) - - def get_as_on_stock(self, item_code, warehouse, posting_date, posting_time): - """Get stock qty on any date""" - bin = sql("select name from tabBin where item_code = %s and warehouse = %s", - (item_code, warehouse)) - if bin: - prev_sle = get_obj('Bin', bin[0][0]).get_prev_sle(posting_date, posting_time) - return flt(prev_sle.get("bin_aqat")) or 0 - else: - return 0 - - def get_incoming_rate(self, item_code=None, warehouse=None, - posting_date=None, posting_time=None, qty=0, serial_no=None, bom_no=None): - in_rate = 0 - - if bom_no: - result = webnotes.conn.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1) - from `tabBOM` where name = %s and docstatus=1 and is_active=1""", - (bom_no,)) - in_rate = result and flt(result[0][0]) or 0 - elif warehouse: - in_rate = get_obj("Valuation Control").get_incoming_rate(posting_date, posting_time, - item_code, warehouse, qty, serial_no) - - return in_rate def validate_incoming_rate(self): for d in getlist(self.doclist, 'mtn_details'): @@ -220,14 +201,15 @@ class DocType(TransactionBase): def update_serial_no(self, is_submit): """Create / Update Serial No""" + from stock.utils import get_valid_serial_nos + sl_obj = get_obj('Stock Ledger') if is_submit: sl_obj.validate_serial_no_warehouse(self, 'mtn_details') for d in getlist(self.doclist, 'mtn_details'): if d.serial_no: - serial_nos = sl_obj.get_sr_no_list(d.serial_no) - for x in serial_nos: + for x in get_valid_serial_nos(d.serial_no): serial_no = x.strip() if d.s_warehouse: sl_obj.update_serial_delivery_details(self, d, serial_no, is_submit) @@ -322,15 +304,17 @@ class DocType(TransactionBase): } return ret - def get_warehouse_details(self, arg): + def get_warehouse_details(self, args): import json - arg, actual_qty, in_rate = json.loads(arg), 0, 0 + args, actual_qty, in_rate = json.loads(args), 0, 0 + args.update({ + posting_date: self.doc.posting_date, + posting_time: self.doc.posting_time + }) + ret = { - "actual_qty" : self.get_as_on_stock(arg.get('item_code'), arg.get('warehouse'), - self.doc.posting_date, self.doc.posting_time), - "incoming_rate" : self.get_incoming_rate(arg.get('item_code'), - arg.get('warehouse'), self.doc.posting_date, self.doc.posting_time, - arg.get('transfer_qty'), arg.get('serial_no'), arg.get('bom_no')) or 0 + "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, + "incoming_rate" : get_incoming_rate(args) } return ret diff --git a/stock/doctype/stock_ledger/stock_ledger.py b/stock/doctype/stock_ledger/stock_ledger.py index 8d39b26dbb..3231850d8c 100644 --- a/stock/doctype/stock_ledger/stock_ledger.py +++ b/stock/doctype/stock_ledger/stock_ledger.py @@ -23,23 +23,10 @@ from webnotes.model.doc import Document from webnotes.model.wrapper import getlist, copy_doclist from webnotes.model.code import get_obj from webnotes import session, msgprint +from stock.utils import get_valid_serial_nos sql = webnotes.conn.sql - -def get_sr_no_list(sr_nos, qty = 0, item_code = ''): - serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') - valid_serial_nos = [] - for val in serial_nos: - if val: - if val in valid_serial_nos: - msgprint("You have entered duplicate serial no: %s" % val, raise_exception=1) - else: - valid_serial_nos.append(val.strip()) - if qty and cstr(sr_nos).strip() and len(valid_serial_nos) != abs(qty): - msgprint("Please enter serial nos for "+ cstr(abs(qty)) + " quantity against item code: " + item_code , raise_exception = 1) - return valid_serial_nos - class DocType: def __init__(self, doc, doclist=[]): self.doc = doc @@ -60,7 +47,7 @@ class DocType: for d in getlist(obj.doclist, fname): wh = d.warehouse or d.s_warehouse if d.serial_no and wh: - serial_nos = self.get_sr_no_list(d.serial_no) + serial_nos = get_valid_serial_nos(d.serial_no) for s in serial_nos: s = s.strip() sr_war = sql("select warehouse,name from `tabSerial No` where name = '%s'" % (s)) @@ -93,10 +80,6 @@ class DocType: if fname == 'purchase_receipt_details' and flt(d.rejected_qty) > 0 and ar_required == 'Yes' and not d.rejected_serial_no: msgprint("Rejected serial no is mandatory for rejected qty of item: "+ d.item_code, raise_exception = 1) - - def get_sr_no_list(self, sr_nos, qty = 0, item_code = ''): - return get_sr_no_list(sr_nos, qty, item_code) - def set_pur_serial_no_values(self, obj, serial_no, d, s, new_rec, rejected=None): item_details = sql("""select item_group, warranty_period @@ -193,7 +176,7 @@ class DocType: import datetime for d in getlist(obj.doclist, fname): if d.serial_no: - serial_nos = self.get_sr_no_list(d.serial_no) + serial_nos = get_valid_serial_nos(d.serial_no) for a in serial_nos: serial_no = a.strip() if is_incoming: @@ -202,7 +185,7 @@ class DocType: self.update_serial_delivery_details(obj, d, serial_no, is_submit) if fname == 'purchase_receipt_details' and d.rejected_qty and d.rejected_serial_no: - serial_nos = self.get_sr_no_list(d.rejected_serial_no) + serial_nos = get_valid_serial_nos(d.rejected_serial_no) for a in serial_nos: self.update_serial_purchase_details(obj, d, a, is_submit, rejected=True) @@ -211,17 +194,17 @@ class DocType: for v in values: sle_id, serial_nos = '', '' # get serial nos - if v["serial_no"]: - serial_nos = self.get_sr_no_list(v["serial_no"], v['actual_qty'], v['item_code']) + if v.get("serial_no"): + serial_nos = get_valid_serial_nos(v["serial_no"], v['actual_qty'], v['item_code']) # reverse quantities for cancel - if v['is_cancelled'] == 'Yes': + if v.get('is_cancelled') == 'Yes': v['actual_qty'] = -flt(v['actual_qty']) # cancel matching entry sql("update `tabStock Ledger Entry` set is_cancelled='Yes' where voucher_no=%s \ and voucher_type=%s", (v['voucher_no'], v['voucher_type'])) - if v["actual_qty"]: + if v.get("actual_qty"): sle_id = self.make_entry(v) args = v.copy() diff --git a/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index fae2fcb9f6..ea9ac12778 100644 --- a/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -21,6 +21,7 @@ from webnotes.utils import cstr, cint, flt, cstr, getdate sql = webnotes.conn.sql msgprint = webnotes.msgprint +from accounts.utils import get_fiscal_year @@ -31,11 +32,11 @@ class DocType: def validate(self): self.validate_mandatory() - self.validate_posting_time() self.validate_item() self.actual_amt_check() self.check_stock_frozen_date() self.scrub_posting_time() + self.doc.fiscal_year = get_fiscal_year(self.doc.posting_date)[0] #check for item quantity available in stock def actual_amt_check(self): @@ -98,13 +99,6 @@ class DocType: if getdate(self.doc.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in webnotes.user.get_roles(): msgprint("You are not authorized to do / modify back dated stock entries before %s" % getdate(stock_frozen_upto).strftime('%d-%m-%Y'), raise_exception=1) - def validate_posting_time(self): - """ Validate posting time format""" - if self.doc.posting_time and len(self.doc.posting_time.split(':')) > 2: - msgprint("Wrong format of posting time, can not complete the transaction. If you think \ - you entered posting time correctly, please contact ERPNext support team.") - raise Exception - def scrub_posting_time(self): if not self.doc.posting_time or self.doc.posting_time == '00:0': self.doc.posting_time = '00:00' diff --git a/stock/doctype/stock_ledger_entry/stock_ledger_entry.txt b/stock/doctype/stock_ledger_entry/stock_ledger_entry.txt index 6b108369e5..988ad33784 100644 --- a/stock/doctype/stock_ledger_entry/stock_ledger_entry.txt +++ b/stock/doctype/stock_ledger_entry/stock_ledger_entry.txt @@ -2,29 +2,29 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2012-03-27 14:36:38", + "creation": "2012-05-03 17:35:06", "modified_by": "Administrator", - "modified": "2012-03-27 14:36:38" + "modified": "2013-01-07 14:15:47" }, { - "section_style": "Simple", "in_create": 1, + "allow_print": 1, "module": "Stock", - "doctype": "DocType", - "server_code_error": " ", + "document_type": "Other", + "allow_email": 1, "autoname": "SLE/.########", "name": "__common__", - "colour": "White:FFF", - "_last_update": "1322549701", - "show_in_menu": 0, - "version": 53, - "hide_toolbar": 1 + "doctype": "DocType", + "hide_toolbar": 1, + "allow_copy": 1 }, { + "read_only": 1, "name": "__common__", "parent": "Stock Ledger Entry", "doctype": "DocField", "parenttype": "DocType", + "permlevel": 0, "parentfield": "fields" }, { @@ -33,6 +33,7 @@ "read": 1, "doctype": "DocPerm", "parenttype": "DocType", + "permlevel": 0, "parentfield": "permissions" }, { @@ -40,57 +41,27 @@ "doctype": "DocType" }, { - "role": "All", - "permlevel": 1, - "doctype": "DocPerm" - }, - { - "amend": 0, - "create": 0, - "doctype": "DocPerm", - "submit": 0, - "write": 0, - "role": "Material User", - "cancel": 0, - "permlevel": 1 - }, - { - "amend": 0, - "create": 0, - "doctype": "DocPerm", - "submit": 0, - "write": 0, - "role": "Material User", - "cancel": 0, - "permlevel": 0 - }, - { - "role": "System Manager", - "permlevel": 2, - "doctype": "DocPerm" - }, - { - "search_index": 1, + "print_width": "100px", + "oldfieldtype": "Link", "doctype": "DocField", "label": "Item Code", "oldfieldname": "item_code", "width": "100px", - "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "oldfieldtype": "Link", + "search_index": 1, "reqd": 0, - "permlevel": 1, + "options": "Item", "in_filter": 1 }, { + "print_width": "100px", "search_index": 0, "doctype": "DocField", "label": "Serial No", "width": "100px", "fieldname": "serial_no", "fieldtype": "Text", - "permlevel": 0, "in_filter": 0 }, { @@ -99,117 +70,101 @@ "label": "Batch No", "oldfieldname": "batch_no", "fieldname": "batch_no", - "fieldtype": "Data", - "permlevel": 0 + "fieldtype": "Data" }, { - "search_index": 1, + "print_width": "100px", + "oldfieldtype": "Link", "doctype": "DocField", "label": "Warehouse", "oldfieldname": "warehouse", "width": "100px", - "options": "Warehouse", "fieldname": "warehouse", "fieldtype": "Link", - "oldfieldtype": "Link", - "permlevel": 1, + "search_index": 1, + "options": "Warehouse", "in_filter": 1 }, { - "search_index": 0, + "oldfieldtype": "Select", "doctype": "DocField", "label": "Warehouse Type", "oldfieldname": "warehouse_type", - "permlevel": 1, "fieldname": "warehouse_type", "fieldtype": "Select", - "oldfieldtype": "Select", + "search_index": 0, "options": "link:Warehouse Type", "in_filter": 1 }, { "description": "The date at which current entry will get or has actually executed.", - "search_index": 1, + "print_width": "100px", + "oldfieldtype": "Date", "doctype": "DocField", "label": "Posting Date", "oldfieldname": "posting_date", "width": "100px", "fieldname": "posting_date", "fieldtype": "Date", - "oldfieldtype": "Date", + "search_index": 1, "reqd": 0, - "permlevel": 1, "in_filter": 1 }, { - "search_index": 0, + "print_width": "100px", + "oldfieldtype": "Time", "doctype": "DocField", "label": "Posting Time", "oldfieldname": "posting_time", "width": "100px", "fieldname": "posting_time", "fieldtype": "Time", - "oldfieldtype": "Time", - "permlevel": 1, + "search_index": 0, "in_filter": 0 }, { - "description": "The date at which current entry is made in system.", - "search_index": 0, - "doctype": "DocField", - "label": "Transaction Date", - "oldfieldname": "transaction_date", - "width": "100px", - "fieldname": "transaction_date", - "fieldtype": "Date", - "oldfieldtype": "Date", - "permlevel": 1, - "in_filter": 1 - }, - { - "search_index": 0, + "print_width": "150px", + "oldfieldtype": "Data", "doctype": "DocField", "label": "Voucher Type", "oldfieldname": "voucher_type", "width": "150px", "fieldname": "voucher_type", "fieldtype": "Data", - "oldfieldtype": "Data", - "permlevel": 1, + "search_index": 0, "in_filter": 1 }, { - "search_index": 0, + "print_width": "150px", + "oldfieldtype": "Data", "doctype": "DocField", "label": "Voucher No", "oldfieldname": "voucher_no", "width": "150px", "fieldname": "voucher_no", "fieldtype": "Data", - "oldfieldtype": "Data", - "permlevel": 1, + "search_index": 0, "in_filter": 1 }, { + "print_width": "150px", "oldfieldtype": "Data", "doctype": "DocField", "label": "Voucher Detail No", "oldfieldname": "voucher_detail_no", "width": "150px", "fieldname": "voucher_detail_no", - "fieldtype": "Data", - "permlevel": 1 + "fieldtype": "Data" }, { + "print_width": "150px", "oldfieldtype": "Currency", - "colour": "White:FFF", "doctype": "DocField", "label": "Actual Quantity", "oldfieldname": "actual_qty", "width": "150px", "fieldname": "actual_qty", "fieldtype": "Currency", - "permlevel": 1, "in_filter": 1 }, { @@ -218,63 +173,38 @@ "label": "Incoming Rate", "oldfieldname": "incoming_rate", "fieldname": "incoming_rate", - "fieldtype": "Currency", - "permlevel": 0 + "fieldtype": "Currency" }, { + "print_width": "150px", "oldfieldtype": "Data", "doctype": "DocField", "label": "Stock UOM", "oldfieldname": "stock_uom", "width": "150px", "fieldname": "stock_uom", - "fieldtype": "Data", - "permlevel": 1 + "fieldtype": "Data" }, { + "print_width": "150px", "oldfieldtype": "Currency", "doctype": "DocField", - "label": "Bin Actual Qty After Transaction", + "label": "Actual Qty After Transaction", "oldfieldname": "bin_aqat", "width": "150px", - "fieldname": "bin_aqat", + "fieldname": "qty_after_transaction", "fieldtype": "Currency", - "permlevel": 1, "in_filter": 1 }, { - "print_hide": 1, - "oldfieldtype": "Currency", - "doctype": "DocField", - "label": "Moving Average Rate", - "oldfieldname": "ma_rate", - "fieldname": "ma_rate", - "fieldtype": "Currency", - "hidden": 1, - "permlevel": 0, - "report_hide": 1 - }, - { - "print_hide": 1, - "oldfieldtype": "Currency", - "doctype": "DocField", - "label": "FIFO Rate", - "oldfieldname": "fcfs_rate", - "fieldname": "fcfs_rate", - "fieldtype": "Currency", - "hidden": 1, - "permlevel": 0, - "report_hide": 1 - }, - { + "print_width": "150px", "oldfieldtype": "Currency", "doctype": "DocField", "label": "Valuation Rate", "oldfieldname": "valuation_rate", "width": "150px", "fieldname": "valuation_rate", - "fieldtype": "Currency", - "permlevel": 0 + "fieldtype": "Currency" }, { "oldfieldtype": "Currency", @@ -282,72 +212,79 @@ "label": "Stock Value", "oldfieldname": "stock_value", "fieldname": "stock_value", - "fieldtype": "Currency", - "permlevel": 0 + "fieldtype": "Currency" }, { "print_hide": 1, "oldfieldtype": "Text", "doctype": "DocField", - "label": "FIFO Stack", + "label": "Stock Queue (FIFO)", "oldfieldname": "fcfs_stack", - "fieldname": "fcfs_stack", + "fieldname": "stock_queue", "fieldtype": "Text", "search_index": 0, "hidden": 1, - "permlevel": 2, "report_hide": 1, "in_filter": 0 }, { - "search_index": 0, + "print_width": "150px", + "oldfieldtype": "Data", "doctype": "DocField", "label": "Company", "oldfieldname": "company", "width": "150px", - "options": "link:Company", "fieldname": "company", "fieldtype": "Select", - "oldfieldtype": "Data", - "permlevel": 1, + "search_index": 0, + "options": "link:Company", "in_filter": 1 }, { - "search_index": 0, + "print_width": "150px", + "oldfieldtype": "Data", "doctype": "DocField", "label": "Fiscal Year", "oldfieldname": "fiscal_year", "width": "150px", "fieldname": "fiscal_year", "fieldtype": "Data", - "oldfieldtype": "Data", - "permlevel": 1, + "search_index": 0, "in_filter": 1 }, { - "search_index": 0, + "print_width": "100px", + "oldfieldtype": "Select", "doctype": "DocField", "label": "Is Cancelled", "oldfieldname": "is_cancelled", "width": "100px", - "options": "\nYes\nNo", "fieldname": "is_cancelled", "fieldtype": "Select", - "oldfieldtype": "Select", - "permlevel": 1, + "search_index": 0, + "options": "\nYes\nNo", "in_filter": 1 }, { - "search_index": 0, - "doctype": "DocField", - "label": "Is Stock Entry", - "oldfieldname": "is_stock_entry", - "width": "100px", - "options": "\nYes\nNo", - "fieldname": "is_stock_entry", - "fieldtype": "Select", - "oldfieldtype": "Select", - "permlevel": 1, - "in_filter": 1 + "amend": 0, + "create": 0, + "doctype": "DocPerm", + "submit": 0, + "write": 0, + "cancel": 0, + "role": "Sales User" + }, + { + "amend": 0, + "create": 0, + "doctype": "DocPerm", + "submit": 0, + "write": 0, + "cancel": 0, + "role": "Material User" + }, + { + "role": "Accounts Manager", + "doctype": "DocPerm" } ] \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/old_stock_reconciliation.py b/stock/doctype/stock_reconciliation/old_stock_reconciliation.py new file mode 100644 index 0000000000..4219f133d9 --- /dev/null +++ b/stock/doctype/stock_reconciliation/old_stock_reconciliation.py @@ -0,0 +1,264 @@ +# 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 . + +from __future__ import unicode_literals +import webnotes +from webnotes.utils import cstr, flt, get_defaults, nowdate, formatdate +from webnotes import msgprint +from webnotes.model.code import get_obj +sql = webnotes.conn.sql + + +class DocType: + def __init__(self, doc, doclist=[]): + self.doc = doc + self.doclist = doclist + self.validated = 1 + self.data = [] + self.val_method = get_defaults()['valuation_method'] + + def get_template(self): + if self.val_method == 'Moving Average': + return [['Item Code', 'Warehouse', 'Quantity', 'Valuation Rate']] + else: + return [['Item Code', 'Warehouse', 'Quantity', 'Incoming Rate']] + + + def read_csv_content(self, submit = 1): + """Get csv data""" + if submit: + from webnotes.utils.datautils import read_csv_content_from_attached_file + data = read_csv_content_from_attached_file(self.doc) + else: + from webnotes.utils.datautils import read_csv_content + data = read_csv_content(self.doc.diff_info) + + return data + + def convert_into_list(self, data, submit=1): + """Convert csv data into list""" + count = 1 + for s in data: + count += 1 + if count == 2 and submit: + if cstr(s[0]).strip() != 'Item Code' or cstr(s[1]).strip() != 'Warehouse': + msgprint("First row of the attachment always should be same as \ + template(Item Code, Warehouse, Quantity \ + and Valuation Rate/Incoming Rate)", raise_exception=1) + else: + continue + # validate + if (submit and len(s) != 4) or (not submit and len(s) != 6): + msgprint("Data entered at Row No " + cstr(count) + " in Attachment File is not in correct format.", raise_exception=1) + self.validated = 0 + self.validate_item(s[0], count) + self.validate_warehouse(s[1], count) + + self.data.append(s) + + if not self.validated: + raise Exception + + + def get_reconciliation_data(self, submit = 1): + """Read and validate csv data""" + data = self.read_csv_content(submit) + self.convert_into_list(data, submit) + + def validate_item(self, item, count): + """ Validate item exists and non-serialized""" + det = sql("select item_code, has_serial_no from `tabItem` where name = %s", cstr(item), as_dict = 1) + if not det: + msgprint("Item: " + cstr(item) + " mentioned at Row No. " + cstr(count) + "does not exist in the system") + self.validated = 0 + elif det and det[0]['has_serial_no'] == 'Yes': + msgprint("""You cannot make Stock Reconciliation of items having serial no. \n + You can directly upload serial no to update their inventory. \n + Please remove Item Code : %s at Row No. %s""" %(cstr(item), cstr(count))) + self.validated = 0 + + + def validate_warehouse(self, wh, count,): + """Validate warehouse exists""" + if not sql("select name from `tabWarehouse` where name = %s", cstr(wh)): + msgprint("Warehouse: " + cstr(wh) + " mentioned at Row No. " + cstr(count) + " does not exist in the system") + self.validated = 0 + + + + def validate(self): + """Validate attachment data""" + if self.doc.file_list: + self.get_reconciliation_data() + + def get_system_stock(self, it, wh): + """get actual qty on reconciliation date and time as per system""" + bin = sql("select name from tabBin where item_code=%s and warehouse=%s", (it, wh)) + prev_sle = bin and get_obj('Bin', bin[0][0]).get_prev_sle(self.doc.reconciliation_date, self.doc.reconciliation_time) or {} + return { + 'actual_qty': prev_sle.get('qty_after_transaction', 0), + 'stock_uom' : sql("select stock_uom from tabItem where name = %s", it)[0][0], + 'val_rate' : prev_sle.get('valuation_rate', 0) + } + + def get_incoming_rate(self, row, qty_diff, sys_stock): + """Calculate incoming rate to maintain valuation rate""" + if qty_diff: + if self.val_method == 'Moving Average': + in_rate = flt(row[3]) + (flt(sys_stock['actual_qty'])*(flt(row[3]) - flt(sys_stock['val_rate'])))/ flt(qty_diff) + elif not sys_stock and not row[3]: + msgprint("Incoming Rate is mandatory for item: %s and warehouse: %s" % (rpw[0], row[1]), raise_exception=1) + else: + in_rate = qty_diff > 0 and row[3] or 0 + else: + in_rate = 0 + + return in_rate + + def make_sl_entry(self, row, qty_diff, sys_stock): + """Make stock ledger entry""" + in_rate = self.get_incoming_rate(row, qty_diff, sys_stock) + values = [{ + 'item_code' : row[0], + 'warehouse' : row[1], + 'transaction_date' : nowdate(), + 'posting_date' : self.doc.reconciliation_date, + 'posting_time' : self.doc.reconciliation_time, + 'voucher_type' : self.doc.doctype, + 'voucher_no' : self.doc.name, + 'voucher_detail_no' : self.doc.name, + 'actual_qty' : flt(qty_diff), + 'stock_uom' : sys_stock['stock_uom'], + 'incoming_rate' : in_rate, + 'company' : get_defaults()['company'], + 'fiscal_year' : get_defaults()['fiscal_year'], + 'is_cancelled' : 'No', + 'batch_no' : '', + 'serial_no' : '' + }] + get_obj('Stock Ledger', 'Stock Ledger').update_stock(values) + + def make_entry_for_valuation(self, row, sys_stock): + self.make_sl_entry(row, 1, sys_stock) + sys_stock['val_rate'] = row[3] + sys_stock['actual_qty'] += 1 + self.make_sl_entry(row, -1, sys_stock) + + def do_stock_reco(self): + """ + Make stock entry of qty diff, calculate incoming rate to maintain valuation rate. + If no qty diff, but diff in valuation rate, make (+1,-1) entry to update valuation + """ + self.diff_info = '' + for row in self.data: + # Get qty as per system + sys_stock = self.get_system_stock(row[0],row[1]) + + # Diff between file and system + qty_diff = row[2] != '~' and flt(row[2]) - flt(sys_stock['actual_qty']) or 0 + rate_diff = row[3] != '~' and flt(row[3]) - flt(sys_stock['val_rate']) or 0 + + # Make sl entry + if qty_diff: + self.make_sl_entry(row, qty_diff, sys_stock) + sys_stock['actual_qty'] += qty_diff + + + if (not qty_diff and rate_diff) or qty_diff < 0 and self.val_method == 'Moving Average': + self.make_entry_for_valuation(row, sys_stock) + + + r = [cstr(i) for i in row] + [cstr(qty_diff), cstr(rate_diff)] + self.store_diff_info(r) + + msgprint("Stock Reconciliation Completed Successfully...") + + def store_diff_info(self, r): + """Add diffs column in attached file""" + + # add header + if not self.diff_info: + if self.val_method == 'Moving Average': + self.diff_info += "Item Code, Warehouse, Qty, Valuation Rate, Qty Diff, Rate Diff" + else: + self.diff_info += "Item Code, Warehouse, Qty, Incoming Rate, Qty Diff, Rate Diff" + + + # add data + self.diff_info += "\n" + ','.join(r) + + webnotes.conn.set(self.doc, 'diff_info', self.diff_info) + + + def on_submit(self): + return + + if not self.doc.file_list: + msgprint("Please attach file before submitting.", raise_exception=1) + else: + self.do_stock_reco() + + + def on_cancel(self): + self.cancel_stock_ledger_entries() + self.update_entries_after() + + def cancel_stock_ledger_entries(self): + webnotes.conn.sql(""" + update `tabStock Ledger Entry` + set is_cancelled = 'Yes' + where voucher_type = 'Stock Reconciliation' and voucher_no = %s + """, self.doc.name) + + def update_entries_after(self): + # get distinct combination of item_code and warehouse to update bin + item_warehouse = webnotes.conn.sql("""select distinct item_code, warehouse + from `tabStock Ledger Entry` where voucher_no = %s and is_cancelled = 'Yes' + and voucher_type = 'Stock Reconciliation'""", self.doc.name) + + from webnotes.model.code import get_obj + errors = [] + for d in item_warehouse: + bin = webnotes.conn.sql("select name from `tabBin` where item_code = %s and \ + warehouse = %s", (d[0], d[1])) + try: + get_obj('Bin', + bin[0][0]).update_entries_after(self.doc.reconciliation_date, + self.doc.reconciliation_time, verbose=0) + except webnotes.ValidationError, e: + errors.append([d[0], d[1], e]) + + if errors: + import re + error_msg = [["Item Code", "Warehouse", "Qty"]] + qty_regex = re.compile(": (.*)") + for e in errors: + qty = qty_regex.findall(unicode(e[2])) + qty = qty and abs(flt(qty[0])) or None + + error_msg.append([e[0], e[1], flt(qty)]) + + webnotes.msgprint("""Your stock is going into negative value \ + in a future transaction. + To cancel, you need to create a stock entry with the \ + following values on %s %s""" % \ + (formatdate(self.doc.reconciliation_date), self.doc.reconciliation_time)) + webnotes.msgprint(error_msg, as_table=1, raise_exception=1) + +@webnotes.whitelist() +def upload(): + from webnotes.utils.datautils import read_csv_content_from_uploaded_file + return read_csv_content_from_uploaded_file() \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.js b/stock/doctype/stock_reconciliation/stock_reconciliation.js index 941b863cfd..3758c59a88 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -13,11 +13,74 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +wn.provide("erpnext.stock"); -cur_frm.cscript.refresh = function(doc) { - if (doc.docstatus) hide_field('steps'); -} +erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ + refresh: function() { + if(this.frm.doc.docstatus===0) { + this.show_download_template(); + this.show_upload(); + } + if(this.frm.doc.reconciliation_json) this.show_reconciliation_data(); + }, + + show_download_template: function() { + var me = this; + this.frm.add_custom_button("Download Template", function() { + this.title = "Stock Reconcilation Template"; + wn.downloadify([["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, this); + return false; + }, "icon-download"); + }, + + show_upload: function() { + var me = this; + var $wrapper = $(cur_frm.fields_dict.upload_html.wrapper).empty(); + var upload_area = $('
').appendTo($wrapper); + + // upload + wn.upload.make({ + parent: $('#dit-upload-area'), + args: { + method: 'stock.doctype.stock_reconciliation.stock_reconciliation.upload', + }, + sample_url: "e.g. http://example.com/somefile.csv", + callback: function(r) { + $wrapper.find(".dit-progress-area").toggle(false); + me.frm.set_value("reconciliation_json", JSON.stringify(r)); + me.show_reconciliation_data(); + } + }); + }, + + show_reconciliation_data: function() { + if(this.frm.doc.reconciliation_json) { + var $wrapper = $(cur_frm.fields_dict.reconciliation_html.wrapper).empty(); + var reconciliation_data = JSON.parse(this.frm.doc.reconciliation_json); -cur_frm.cscript.download_template = function(doc, cdt, cdn) { - $c_obj_csv(make_doclist(cdt, cdn), 'get_template', ''); -} + var _make = function(data, header) { + var result = ""; + + var _render = header + ? function(col) { return "" + col + "" } + : function(col) { return "" + col + "" }; + + $.each(data, function(i, row) { + result += "" + + $.map(row, _render).join("") + + ""; + }); + return result; + } + + var $reconciliation_table = $("
\ + \ + " + _make([reconciliation_data[0]], true) + "\ + " + _make(reconciliation_data.splice(1)) + "\ +
\ +
").appendTo($wrapper); + } + }, +}); + +cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 7df4b1601f..7db36e7ac1 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -16,243 +16,158 @@ from __future__ import unicode_literals import webnotes -from webnotes.utils import cstr, flt, get_defaults, nowdate, formatdate -from webnotes import msgprint -from webnotes.model.code import get_obj -sql = webnotes.conn.sql - +import json +from webnotes import msgprint, _ +from webnotes.utils import cstr, flt + + class DocType: def __init__(self, doc, doclist=[]): self.doc = doc self.doclist = doclist - self.validated = 1 - self.data = [] - self.val_method = get_defaults()['valuation_method'] - - def get_template(self): - if self.val_method == 'Moving Average': - return [['Item Code', 'Warehouse', 'Quantity', 'Valuation Rate']] - else: - return [['Item Code', 'Warehouse', 'Quantity', 'Incoming Rate']] - - - def read_csv_content(self, submit = 1): - """Get csv data""" - if submit: - from webnotes.utils.datautils import read_csv_content_from_attached_file - data = read_csv_content_from_attached_file(self.doc) - else: - from webnotes.utils.datautils import read_csv_content - data = read_csv_content(self.doc.diff_info) - - return data - - def convert_into_list(self, data, submit=1): - """Convert csv data into list""" - count = 1 - for s in data: - count += 1 - if count == 2 and submit: - if cstr(s[0]).strip() != 'Item Code' or cstr(s[1]).strip() != 'Warehouse': - msgprint("First row of the attachment always should be same as \ - template(Item Code, Warehouse, Quantity \ - and Valuation Rate/Incoming Rate)", raise_exception=1) - else: - continue - # validate - if (submit and len(s) != 4) or (not submit and len(s) != 6): - msgprint("Data entered at Row No " + cstr(count) + " in Attachment File is not in correct format.", raise_exception=1) - self.validated = 0 - self.validate_item(s[0], count) - self.validate_warehouse(s[1], count) - - self.data.append(s) - - if not self.validated: - raise Exception - - - def get_reconciliation_data(self, submit = 1): - """Read and validate csv data""" - data = self.read_csv_content(submit) - self.convert_into_list(data, submit) - def validate_item(self, item, count): - """ Validate item exists and non-serialized""" - det = sql("select item_code, has_serial_no from `tabItem` where name = %s", cstr(item), as_dict = 1) - if not det: - msgprint("Item: " + cstr(item) + " mentioned at Row No. " + cstr(count) + "does not exist in the system") - self.validated = 0 - elif det and det[0]['has_serial_no'] == 'Yes': - msgprint("""You cannot make Stock Reconciliation of items having serial no. \n - You can directly upload serial no to update their inventory. \n - Please remove Item Code : %s at Row No. %s""" %(cstr(item), cstr(count))) - self.validated = 0 - - - def validate_warehouse(self, wh, count,): - """Validate warehouse exists""" - if not sql("select name from `tabWarehouse` where name = %s", cstr(wh)): - msgprint("Warehouse: " + cstr(wh) + " mentioned at Row No. " + cstr(count) + " does not exist in the system") - self.validated = 0 - - - def validate(self): - """Validate attachment data""" - if self.doc.file_list: - self.get_reconciliation_data() - - def get_system_stock(self, it, wh): - """get actual qty on reconciliation date and time as per system""" - bin = sql("select name from tabBin where item_code=%s and warehouse=%s", (it, wh)) - prev_sle = bin and get_obj('Bin', bin[0][0]).get_prev_sle(self.doc.reconciliation_date, self.doc.reconciliation_time) or {} - return { - 'actual_qty': prev_sle.get('bin_aqat', 0), - 'stock_uom' : sql("select stock_uom from tabItem where name = %s", it)[0][0], - 'val_rate' : prev_sle.get('valuation_rate', 0) - } - - def get_incoming_rate(self, row, qty_diff, sys_stock): - """Calculate incoming rate to maintain valuation rate""" - if qty_diff: - if self.val_method == 'Moving Average': - in_rate = flt(row[3]) + (flt(sys_stock['actual_qty'])*(flt(row[3]) - flt(sys_stock['val_rate'])))/ flt(qty_diff) - elif not sys_stock and not row[3]: - msgprint("Incoming Rate is mandatory for item: %s and warehouse: %s" % (rpw[0], row[1]), raise_exception=1) - else: - in_rate = qty_diff > 0 and row[3] or 0 - else: - in_rate = 0 - - return in_rate - - def make_sl_entry(self, row, qty_diff, sys_stock): - """Make stock ledger entry""" - in_rate = self.get_incoming_rate(row, qty_diff, sys_stock) - values = [{ - 'item_code' : row[0], - 'warehouse' : row[1], - 'transaction_date' : nowdate(), - 'posting_date' : self.doc.reconciliation_date, - 'posting_time' : self.doc.reconciliation_time, - 'voucher_type' : self.doc.doctype, - 'voucher_no' : self.doc.name, - 'voucher_detail_no' : self.doc.name, - 'actual_qty' : flt(qty_diff), - 'stock_uom' : sys_stock['stock_uom'], - 'incoming_rate' : in_rate, - 'company' : get_defaults()['company'], - 'fiscal_year' : get_defaults()['fiscal_year'], - 'is_cancelled' : 'No', - 'batch_no' : '', - 'serial_no' : '' - }] - get_obj('Stock Ledger', 'Stock Ledger').update_stock(values) + self.validate_data() - def make_entry_for_valuation(self, row, sys_stock): - self.make_sl_entry(row, 1, sys_stock) - sys_stock['val_rate'] = row[3] - sys_stock['actual_qty'] += 1 - self.make_sl_entry(row, -1, sys_stock) - - def do_stock_reco(self): - """ - Make stock entry of qty diff, calculate incoming rate to maintain valuation rate. - If no qty diff, but diff in valuation rate, make (+1,-1) entry to update valuation - """ - self.diff_info = '' - for row in self.data: - # Get qty as per system - sys_stock = self.get_system_stock(row[0],row[1]) - - # Diff between file and system - qty_diff = row[2] != '~' and flt(row[2]) - flt(sys_stock['actual_qty']) or 0 - rate_diff = row[3] != '~' and flt(row[3]) - flt(sys_stock['val_rate']) or 0 - - # Make sl entry - if qty_diff: - self.make_sl_entry(row, qty_diff, sys_stock) - sys_stock['actual_qty'] += qty_diff - - - if (not qty_diff and rate_diff) or qty_diff < 0 and self.val_method == 'Moving Average': - self.make_entry_for_valuation(row, sys_stock) - - - r = [cstr(i) for i in row] + [cstr(qty_diff), cstr(rate_diff)] - self.store_diff_info(r) - - msgprint("Stock Reconciliation Completed Successfully...") - - def store_diff_info(self, r): - """Add diffs column in attached file""" - - # add header - if not self.diff_info: - if self.val_method == 'Moving Average': - self.diff_info += "Item Code, Warehouse, Qty, Valuation Rate, Qty Diff, Rate Diff" - else: - self.diff_info += "Item Code, Warehouse, Qty, Incoming Rate, Qty Diff, Rate Diff" - - - # add data - self.diff_info += "\n" + ','.join(r) - - webnotes.conn.set(self.doc, 'diff_info', self.diff_info) - - def on_submit(self): - if not self.doc.file_list: - msgprint("Please attach file before submitting.", raise_exception=1) - else: - self.do_stock_reco() - - + self.create_stock_ledger_entries() + def on_cancel(self): - self.cancel_stock_ledger_entries() - self.update_entries_after() + pass - def cancel_stock_ledger_entries(self): - webnotes.conn.sql(""" - update `tabStock Ledger Entry` - set is_cancelled = 'Yes' - where voucher_type = 'Stock Reconciliation' and voucher_no = %s - """, self.doc.name) - - def update_entries_after(self): - # get distinct combination of item_code and warehouse to update bin - item_warehouse = webnotes.conn.sql("""select distinct item_code, warehouse - from `tabStock Ledger Entry` where voucher_no = %s and is_cancelled = 'Yes' - and voucher_type = 'Stock Reconciliation'""", self.doc.name) - - from webnotes.model.code import get_obj - errors = [] - for d in item_warehouse: - bin = webnotes.conn.sql("select name from `tabBin` where item_code = %s and \ - warehouse = %s", (d[0], d[1])) - try: - get_obj('Bin', - bin[0][0]).update_entries_after(self.doc.reconciliation_date, - self.doc.reconciliation_time, verbose=0) - except webnotes.ValidationError, e: - errors.append([d[0], d[1], e]) - - if errors: - import re - error_msg = [["Item Code", "Warehouse", "Qty"]] - qty_regex = re.compile(": (.*)") - for e in errors: - qty = qty_regex.findall(unicode(e[2])) - qty = qty and abs(flt(qty[0])) or None + def validate_data(self): + data = json.loads(self.doc.reconciliation_json) + if data[0] != ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]: + msgprint(_("""Hey! You seem to be using the wrong template. \ + Click on 'Download Template' button to get the correct template."""), + raise_exception=1) - error_msg.append([e[0], e[1], flt(qty)]) + def _get_msg(row_num, msg): + return _("Row # ") + ("%d: " % (row_num+2)) + _(msg) + + self.validation_messages = [] + item_warehouse_combinations = [] + for row_num, row in enumerate(data[1:]): + # find duplicates + if [row[0], row[1]] in item_warehouse_combinations: + self.validation_messages.append(_get_msg(row_num, "Duplicate entry")) + else: + item_warehouse_combinations.append([row[0], row[1]]) - webnotes.msgprint("""Your stock is going into negative value \ - in a future transaction. - To cancel, you need to create a stock entry with the \ - following values on %s %s""" % \ - (formatdate(self.doc.reconciliation_date), self.doc.reconciliation_time)) - webnotes.msgprint(error_msg, as_table=1, raise_exception=1) - \ No newline at end of file + self.validate_item(row[0], row_num) + # note: warehouse will be validated through link validation + + # if both not specified + if row[2] == "" and row[3] == "": + self.validation_messages.append(_get_msg(row_num, + "Please specify either Quantity or Valuation Rate or both")) + + # do not allow negative quantity + if flt(row[2]) < 0: + self.validation_messages.append(_get_msg(row_num, + "Negative Quantity is not allowed")) + + # do not allow negative valuation + if flt(row[3]) < 0: + self.validation_messages.append(_get_msg(row_num, + "Negative Valuation Rate is not allowed")) + + # throw all validation messages + if self.validation_messages: + for msg in self.validation_messages: + msgprint(msg) + + raise webnotes.ValidationError + + def validate_item(self, item_code, row_num): + from stock.utils import validate_end_of_life, validate_is_stock_item, \ + validate_cancelled_item + + # using try except to catch all validation msgs and display together + + try: + item = webnotes.doc("Item", item_code) + + # end of life and stock item + validate_end_of_life(item_code, item.end_of_life, verbose=0) + validate_is_stock_item(item_code, item.is_stock_item, verbose=0) + + # item should not be serialized + if item.has_serial_no == "Yes": + raise webnotes.ValidationError, (_("Serialized Item: '") + item_code + + _("""' can not be managed using Stock Reconciliation.\ + You can add/delete Serial No directly, to modify stock of this item.""")) + + # docstatus should be < 2 + validate_cancelled_item(item_code, item.docstatus, verbose=0) + + except Exception, e: + self.validation_messages.append(_("Row # ") + ("%d: " % (row_num+2)) + cstr(e)) + + def create_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): + return (qty * valuation_rate - previous_qty * previous_valuation_rate) \ + / flt(qty - previous_qty) + + row_template = ["item_code", "warehouse", "qty", "valuation_rate"] + + data = json.loads(self.doc.reconciliation_json) + for row_num, row in enumerate(data[1:]): + row = webnotes._dict(zip(row_template, row)) + + args = { + "__islocal": 1, + "item_code": row[0], + "warehouse": row[1], + "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") + } + previous_sle = get_previous_sle(args) + + qty_diff = _qty_diff(row[2], previous_sle) + + if get_valuation_method(row[0]) == "Moving Average": + rate_diff = _rate_diff(row[3], previous_sle) + if qty_diff: + actual_qty = qty_diff, + if flt(previous_sle.valuation_rate): + incoming_rate = _get_incoming_rate(flt(row[2]), flt(row[3]), + flt(previous_sle.qty_after_transaction), + flt(previous_sle.valuation_rate)) + else: + incoming_rate = row[3] + + webnotes.model_wrapper([args]).save() + elif rate_diff: + # make +1, -1 entry + pass + + else: + # FIFO + # Make reverse entry + + # make entry as per attachment + pass + + + + + +@webnotes.whitelist() +def upload(): + from webnotes.utils.datautils import read_csv_content_from_uploaded_file + return read_csv_content_from_uploaded_file() \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.txt b/stock/doctype/stock_reconciliation/stock_reconciliation.txt index 974e0c079d..f5cf6235f7 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.txt +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.txt @@ -2,32 +2,29 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2012-04-13 11:56:39", + "creation": "2013-01-04 13:57:25", "modified_by": "Administrator", - "modified": "2012-05-10 11:54:52" + "modified": "2013-01-04 13:58:54" }, { - "section_style": "Tray", - "allow_attach": 1, "is_submittable": 1, + "allow_attach": 0, + "allow_print": 1, "search_fields": "reconciliation_date", "module": "Stock", - "server_code_error": " ", - "subject": "Date: %(reconciliation_date)s, Time: %(reconciliation_time)s", - "_last_update": "1321617741", + "allow_email": 1, "autoname": "SR/.######", "name": "__common__", - "colour": "White:FFF", "doctype": "DocType", - "show_in_menu": 0, "max_attachments": 1, - "version": 1 + "allow_copy": 1 }, { "name": "__common__", "parent": "Stock Reconciliation", "doctype": "DocField", "parenttype": "DocType", + "permlevel": 0, "parentfield": "fields" }, { @@ -36,6 +33,7 @@ "read": 1, "doctype": "DocPerm", "parenttype": "DocType", + "role": "Material Manager", "parentfield": "permissions" }, { @@ -43,12 +41,97 @@ "doctype": "DocType" }, { - "amend": 0, + "read_only": 0, + "oldfieldtype": "Date", + "doctype": "DocField", + "label": "Posting Date", + "oldfieldname": "reconciliation_date", + "fieldname": "posting_date", + "fieldtype": "Date", + "reqd": 1, + "in_filter": 0 + }, + { + "read_only": 0, + "oldfieldtype": "Time", + "doctype": "DocField", + "label": "Posting Time", + "oldfieldname": "reconciliation_time", + "fieldname": "posting_time", + "fieldtype": "Time", + "reqd": 1, + "in_filter": 0 + }, + { + "read_only": 1, + "print_hide": 1, + "no_copy": 1, + "doctype": "DocField", + "label": "Amended From", + "fieldname": "amended_from", + "fieldtype": "Link", + "options": "Stock Reconciliation" + }, + { + "doctype": "DocField", + "fieldname": "col1", + "fieldtype": "Column Break" + }, + { + "read_only": 0, + "oldfieldtype": "Text", + "doctype": "DocField", + "label": "Remark", + "oldfieldname": "remark", + "fieldname": "remark", + "fieldtype": "Text" + }, + { + "depends_on": "eval:doc.docstatus===0", + "doctype": "DocField", + "label": "Upload", + "fieldname": "sb1", + "fieldtype": "Section Break" + }, + { + "read_only": 1, + "print_hide": 1, + "doctype": "DocField", + "label": "Upload HTML", + "fieldname": "upload_html", + "fieldtype": "HTML" + }, + { + "doctype": "DocField", + "label": "Reconciliation Data", + "fieldname": "sb2", + "fieldtype": "Section Break" + }, + { + "read_only": 1, + "print_hide": 0, + "doctype": "DocField", + "label": "Reconciliation HTML", + "fieldname": "reconciliation_html", + "fieldtype": "HTML", + "hidden": 0 + }, + { + "read_only": 1, + "print_hide": 1, + "no_copy": 1, + "doctype": "DocField", + "label": "Reconciliation JSON", + "fieldname": "reconciliation_json", + "fieldtype": "Text", + "hidden": 1 + }, + { + "amend": 1, "create": 1, "doctype": "DocPerm", "submit": 1, "write": 1, - "role": "Material Manager", "cancel": 1, "permlevel": 0 }, @@ -58,105 +141,7 @@ "doctype": "DocPerm", "submit": 0, "write": 0, - "role": "Material Manager", "cancel": 0, "permlevel": 1 - }, - { - "create": 1, - "doctype": "DocPerm", - "submit": 1, - "write": 1, - "role": "System Manager", - "cancel": 1, - "permlevel": 0 - }, - { - "doctype": "DocField", - "options": "
Steps:
1. Enter Reconciliation Date and Time
2. Save the document
3. Attach csv file as per template.
4. Submit the document
5. Enter tilde (~) sign if no difference in qty or valuation rate
", - "fieldname": "steps", - "fieldtype": "HTML", - "label": "Steps", - "permlevel": 0 - }, - { - "oldfieldtype": "Date", - "doctype": "DocField", - "label": "Reconciliation Date", - "oldfieldname": "reconciliation_date", - "fieldname": "reconciliation_date", - "fieldtype": "Date", - "reqd": 1, - "permlevel": 0, - "in_filter": 0 - }, - { - "oldfieldtype": "Time", - "doctype": "DocField", - "label": "Reconciliation Time", - "oldfieldname": "reconciliation_time", - "fieldname": "reconciliation_time", - "fieldtype": "Time", - "reqd": 1, - "permlevel": 0, - "in_filter": 0 - }, - { - "oldfieldtype": "Text", - "doctype": "DocField", - "label": "Remark", - "oldfieldname": "remark", - "fieldname": "remark", - "fieldtype": "Text", - "permlevel": 0 - }, - { - "doctype": "DocField", - "label": "Download Template", - "fieldname": "download_template", - "fieldtype": "Button", - "permlevel": 0 - }, - { - "print_hide": 1, - "no_copy": 1, - "oldfieldtype": "Text", - "doctype": "DocField", - "label": "File List", - "oldfieldname": "file_list", - "fieldname": "file_list", - "fieldtype": "Text", - "hidden": 1, - "permlevel": 1 - }, - { - "print_hide": 1, - "doctype": "DocField", - "label": "Diff Info", - "fieldname": "diff_info", - "fieldtype": "Text", - "hidden": 1, - "permlevel": 0 - }, - { - "print_hide": 1, - "description": "The date at which current entry is corrected in the system.", - "no_copy": 1, - "depends_on": "eval:doc.amended_from", - "doctype": "DocField", - "label": "Amendment Date", - "fieldname": "amendment_date", - "fieldtype": "Date", - "permlevel": 0 - }, - { - "print_hide": 1, - "no_copy": 1, - "doctype": "DocField", - "label": "Amended From", - "permlevel": 1, - "fieldname": "amended_from", - "fieldtype": "Link", - "options": "Sales Invoice" } ] \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py new file mode 100644 index 0000000000..b967b8a54b --- /dev/null +++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -0,0 +1,125 @@ +# 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 . + + +from __future__ import unicode_literals +import unittest +import webnotes +from webnotes.tests import insert_test_data +import json +from accounts.utils import get_fiscal_year +from pprint import pprint + +company = webnotes.conn.get_default("company") + +class TestStockReconciliation(unittest.TestCase): + def setUp(self): + webnotes.conn.begin() + self.insert_test_data() + + def tearDown(self): + print "Message Log:", webnotes.message_log + webnotes.conn.rollback() + + def test_reco_for_fifo(self): + webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", "FIFO") + self.submit_stock_reconciliation("2012-12-26", "12:05", 50, 1000) + + res = webnotes.conn.sql("""select stock_queue from `tabStock Ledger Entry` + where item_code = 'Android Jack D' and warehouse = 'Default Warehouse' + and voucher_no = 'RECO-001'""") + + self.assertEqual(res[0][0], [[50, 1000]]) + + def test_reco_for_moving_average(self): + webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", "Moving Average") + + def submit_stock_reconciliation(self, posting_date, posting_time, qty, rate): + return webnotes.model_wrapper([{ + "doctype": "Stock Reconciliation", + "name": "RECO-001", + "__islocal": 1, + "posting_date": posting_date, + "posting_time": posting_time, + "reconciliation_json": json.dumps([ + ["Item Code", "Warehouse", "Quantity", "Valuation Rate"], + ["Android Jack D", "Default Warehouse", qty, rate] + ]), + }]).submit() + + def insert_test_data(self): + # create item groups and items + insert_test_data("Item Group", + sort_fn=lambda ig: (ig[0].get('parent_item_group'), ig[0].get('name'))) + insert_test_data("Item") + + # create default warehouse + if not webnotes.conn.exists("Warehouse", "Default Warehouse"): + webnotes.insert({"doctype": "Warehouse", + "warehouse_name": "Default Warehouse", + "warehouse_type": "Stores"}) + + # create UOM: Nos. + if not webnotes.conn.exists("UOM", "Nos"): + webnotes.insert({"doctype": "UOM", "uom_name": "Nos"}) + + existing_ledgers = [ + { + "doctype": "Stock Ledger Entry", "__islocal": 1, + "voucher_type": "Stock Entry", "voucher_no": "TEST", + "item_code": "Android Jack D", "warehouse": "Default Warehouse", + "posting_date": "2012-12-12", "posting_time": "01:00:00", + "actual_qty": 20, "incoming_rate": 1000, "company": company + }, + { + "doctype": "Stock Ledger Entry", "__islocal": 1, + "voucher_type": "Stock Entry", "voucher_no": "TEST", + "item_code": "Android Jack D", "warehouse": "Default Warehouse", + "posting_date": "2012-12-15", "posting_time": "02:00:00", + "actual_qty": 10, "incoming_rate": 700, "company": company + }, + { + "doctype": "Stock Ledger Entry", "__islocal": 1, + "voucher_type": "Stock Entry", "voucher_no": "TEST", + "item_code": "Android Jack D", "warehouse": "Default Warehouse", + "posting_date": "2012-12-25", "posting_time": "03:00:00", + "actual_qty": -15, "company": company + }, + { + "doctype": "Stock Ledger Entry", "__islocal": 1, + "voucher_type": "Stock Entry", "voucher_no": "TEST", + "item_code": "Android Jack D", "warehouse": "Default Warehouse", + "posting_date": "2012-12-31", "posting_time": "08:00:00", + "actual_qty": -20, "company": company + }, + { + "doctype": "Stock Ledger Entry", "__islocal": 1, + "voucher_type": "Stock Entry", "voucher_no": "TEST", + "item_code": "Android Jack D", "warehouse": "Default Warehouse", + "posting_date": "2013-01-05", "posting_time": "07:00:00", + "actual_qty": 15, "incoming_rate": 1200, "company": company + }, + ] + + pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' + and warehouse='Default Warehouse'""", as_dict=1)) + + webnotes.get_obj("Stock Ledger").update_stock(existing_ledgers) + + pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' + and warehouse='Default Warehouse'""", as_dict=1)) + + \ No newline at end of file diff --git a/stock/doctype/valuation_control/__init__.py b/stock/doctype/valuation_control/__init__.py deleted file mode 100644 index baffc48825..0000000000 --- a/stock/doctype/valuation_control/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/stock/doctype/valuation_control/valuation_control.py b/stock/doctype/valuation_control/valuation_control.py deleted file mode 100644 index 3953f53da7..0000000000 --- a/stock/doctype/valuation_control/valuation_control.py +++ /dev/null @@ -1,131 +0,0 @@ -# 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 . - -from __future__ import unicode_literals -import webnotes, unittest - -from webnotes.utils import flt -from webnotes.model.code import get_obj - -class TestValuationControl(unittest.TestCase): - def setUp(self): - webnotes.conn.begin() - - def tearDown(self): - webnotes.conn.rollback() - - def test_fifo_rate(self): - """test fifo rate""" - fcfs_stack = [[40,500.0], [12,400.0]] - self.assertTrue(DocType(None, None).get_fifo_rate(fcfs_stack)==((40*500.0 + 12*400.0)/52.0)) - - def test_serial_no_value(self): - """test serial no value""" - from webnotes.model.doc import Document - - Document(fielddata = { - 'doctype': 'Item', - 'docstatus': 0, - 'name': 'it', - 'item_name': 'it', - 'item_code': 'it', - 'item_group': 'Default', - 'is_stock_item': 'Yes', - 'has_serial_no': 'Yes', - 'stock_uom': 'Nos', - 'is_sales_item': 'Yes', - 'is_purchase_item': 'Yes', - 'is_service_item': 'No', - 'is_sub_contracted_item': 'No', - 'is_pro_applicable': 'Yes', - 'is_manufactured_item': 'Yes' - }).save(1) - - s1 = Document(fielddata= { - 'doctype':'Serial No', - 'serial_no':'s1', - 'item_code':'it', - 'purchase_rate': 100.0 - }) - s2 = Document(fielddata = s1.fields.copy()) - s3 = Document(fielddata = s1.fields.copy()) - s4 = Document(fielddata = s1.fields.copy()) - s1.save(1) - s2.purchase_rate = 120.0 - s2.serial_no = 's2' - s2.save(1) - s3.purchase_rate = 130.0 - s3.serial_no = 's3' - s3.save(1) - s4.purchase_rate = 150.0 - s4.serial_no = 's4' - s4.save(1) - - r = DocType(None, None).get_serializable_inventory_rate('s1,s2,s3') - self.assertTrue(flt(r) - (100.0+120.0+130.0)/3 < 0.0001) - - -class DocType: - def __init__(self, d, dl): - self.doc, self.doclist = d, dl - - def get_fifo_rate(self, fcfs_stack): - """get FIFO (average) Rate from Stack""" - if not fcfs_stack: - return 0.0 - - total = sum(f[0] for f in fcfs_stack) - if not total: - return 0.0 - - return sum(f[0] * f[1] for f in fcfs_stack) / total - - def get_serializable_inventory_rate(self, serial_no): - """get average value of serial numbers""" - - sr_nos = get_obj("Stock Ledger").get_sr_no_list(serial_no) - return webnotes.conn.sql("""select avg(ifnull(purchase_rate, 0)) - from `tabSerial No` where name in ("%s")""" % '", "'.join(sr_nos))[0][0] or 0.0 - - - def get_valuation_method(self, item_code): - """get valuation method from item or default""" - val_method = webnotes.conn.get_value('Item', item_code, 'valuation_method') - if not val_method: - from webnotes.utils import get_defaults - val_method = get_defaults().get('valuation_method', 'FIFO') - return val_method - - - def get_incoming_rate(self, posting_date, posting_time, item, warehouse, qty = 0, serial_no = ''): - """Get Incoming Rate based on valuation method""" - in_rate = 0 - val_method = self.get_valuation_method(item) - bin_obj = get_obj('Warehouse',warehouse).get_bin(item) - if serial_no: - in_rate = self.get_serializable_inventory_rate(serial_no) - elif val_method == 'FIFO': - # get rate based on the last item value? - if qty: - prev_sle = bin_obj.get_prev_sle(posting_date, posting_time) - if not prev_sle: - return 0.0 - fcfs_stack = eval(str(prev_sle.get('fcfs_stack', '[]'))) - in_rate = fcfs_stack and self.get_fifo_rate(fcfs_stack) or 0 - elif val_method == 'Moving Average': - prev_sle = bin_obj.get_prev_sle(posting_date, posting_time) - in_rate = prev_sle and prev_sle.get('valuation_rate', 0) or 0 - return in_rate diff --git a/stock/doctype/valuation_control/valuation_control.txt b/stock/doctype/valuation_control/valuation_control.txt deleted file mode 100644 index 3a207642f2..0000000000 --- a/stock/doctype/valuation_control/valuation_control.txt +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "owner": "Administrator", - "docstatus": 0, - "creation": "2012-03-27 14:36:40", - "modified_by": "Administrator", - "modified": "2012-03-27 14:36:40" - }, - { - "section_style": "Simple", - "in_create": 1, - "colour": "White:FFF", - "module": "Stock", - "server_code_error": " ", - "version": 4, - "doctype": "DocType", - "issingle": 1, - "name": "__common__" - }, - { - "name": "Valuation Control", - "doctype": "DocType" - } -] \ No newline at end of file diff --git a/stock/report/stock_ledger/stock_ledger.txt b/stock/report/stock_ledger/stock_ledger.txt index ff03168ad3..68f44b7b68 100644 --- a/stock/report/stock_ledger/stock_ledger.txt +++ b/stock/report/stock_ledger/stock_ledger.txt @@ -10,7 +10,7 @@ "name": "__common__", "ref_doctype": "Stock Ledger Entry", "doctype": "Report", - "json": "{\"filters\":[[\"Stock Ledger Entry\",\"is_cancelled\",\"=\",\"No\"]],\"columns\":[[\"item_code\",\"Stock Ledger Entry\"],[\"warehouse\",\"Stock Ledger Entry\"],[\"posting_date\",\"Stock Ledger Entry\"],[\"posting_time\",\"Stock Ledger Entry\"],[\"actual_qty\",\"Stock Ledger Entry\"],[\"bin_aqat\",\"Stock Ledger Entry\"],[\"voucher_type\",\"Stock Ledger Entry\"],[\"voucher_no\",\"Stock Ledger Entry\"]],\"sort_by\":\"Stock Ledger Entry.posting_date\",\"sort_order\":\"desc\",\"sort_by_next\":\"Stock Ledger Entry.posting_time\",\"sort_order_next\":\"desc\"}" + "json": "{\"filters\":[[\"Stock Ledger Entry\",\"is_cancelled\",\"=\",\"No\"]],\"columns\":[[\"item_code\",\"Stock Ledger Entry\"],[\"warehouse\",\"Stock Ledger Entry\"],[\"posting_date\",\"Stock Ledger Entry\"],[\"posting_time\",\"Stock Ledger Entry\"],[\"actual_qty\",\"Stock Ledger Entry\"],[\"qty_after_transaction\",\"Stock Ledger Entry\"],[\"voucher_type\",\"Stock Ledger Entry\"],[\"voucher_no\",\"Stock Ledger Entry\"]],\"sort_by\":\"Stock Ledger Entry.posting_date\",\"sort_order\":\"desc\",\"sort_by_next\":\"Stock Ledger Entry.posting_time\",\"sort_order_next\":\"desc\"}" }, { "name": "Stock Ledger", diff --git a/stock/utils.py b/stock/utils.py new file mode 100644 index 0000000000..28919c97fd --- /dev/null +++ b/stock/utils.py @@ -0,0 +1,157 @@ +# 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, _ +import json +from webnotes.utils import flt + +def validate_end_of_life(item_code, end_of_life=None, verbose=1): + if not end_of_life: + end_of_life = webnotes.conn.get_value("Item", item_code, "end_of_life") + + from webnotes.utils import getdate, now_datetime, formatdate + if end_of_life and getdate(end_of_life) > now_datetime().date(): + msg = (_("Item") + " %(item_code)s: " + _("reached its end of life on") + \ + " %(date)s. " + _("Please check") + ": %(end_of_life_label)s " + \ + "in Item master") % { + "item_code": item_code, + "date": formatdate(end_of_life), + "end_of_life_label": webnotes.get_label("Item", "end_of_life") + } + + _msgprint(msg, verbose) + +def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): + if not is_stock_item: + is_stock_item = webnotes.conn.get_value("Item", item_code, "is_stock_item") + + if is_stock_item != "Yes": + msg = (_("Item") + " %(item_code)s: " + _("is not a Stock Item")) % { + "item_code": item_code, + } + + _msgprint(msg, verbose) + +def validate_cancelled_item(item_code, docstatus=None, verbose=1): + if docstatus is None: + docstatus = webnotes.conn.get_value("Item", item_code, "docstatus") + + if docstatus == 2: + msg = (_("Item") + " %(item_code)s: " + _("is a cancelled Item")) % { + "item_code": item_code, + } + + _msgprint(msg, verbose) + +def _msgprint(msg, verbose): + if verbose: + msgprint(msg, raise_exception=True) + else: + raise webnotes.ValidationError, msg + +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 + """ + if not args.get("posting_date"): + args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): + args["posting_time"] = "12:00" + + sle = sql(""" + select * from `tabStock Ledger Entry` + where item_code = %(item_code)s + and warehouse = %(warehouse)s + and ifnull(is_cancelled, 'No') = 'No' + and name != %(sle)s + and timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s) + order by timestamp(posting_date, posting_time) desc, name desc + limit 1 + """, args, as_dict=1) + + return sle and sle[0] or {} + +def get_incoming_rate(args): + """Get Incoming Rate based on valuation method""" + + in_rate = 0 + if args.get("serial_no"): + in_rate = get_avg_purchase_rate(args.get("serial_no")) + elif args.get("bom_no"): + result = webnotes.conn.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1) + from `tabBOM` where name = %s and docstatus=1 and is_active=1""", args.get("bom_no")) + in_rate = result and flt(result[0][0]) or 0 + else: + valuation_method = get_valuation_method(args.get("item_code")) + previous_sle = get_previous_sle(args) + if valuation_method == 'FIFO': + # get rate based on the last item value? + if args.get("qty"): + if not previous_sle: + return 0.0 + stock_queue = json.loads(previous_sle.get('stock_queue', '[]')) + in_rate = stock_queue and get_fifo_rate(stock_queue) or 0 + elif valuation_method == 'Moving Average': + in_rate = previous_sle.get('valuation_rate') or 0 + return in_rate + +def get_avg_purchase_rate(serial_nos): + """get average value of serial numbers""" + + serial_nos = get_valid_serial_nos(serial_nos) + return 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]) + +def get_valuation_method(item_code): + """get valuation method from item or default""" + val_method = webnotes.conn.get_value('Item', item_code, 'valuation_method') + if not val_method: + from webnotes.utils import get_defaults + val_method = get_defaults().get('valuation_method', 'FIFO') + return val_method + +def get_fifo_rate(stock_queue): + """get FIFO (average) Rate from Stack""" + if not stock_queue: + return 0.0 + + total = sum(f[0] for f in stock_queue) + return total and sum(f[0] * f[1] for f in stock_queue) / flt(total) or 0.0 + +def get_valid_serial_nos(sr_nos, qty=0, item_code=''): + """split serial nos, validate and return list of valid serial nos""" + # TODO: remove duplicates in client side + serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') + + valid_serial_nos = [] + for val in serial_nos: + if val: + val = val.strip() + if val in valid_serial_nos: + msgprint("You have entered duplicate serial no: '%s'" % val, raise_exception=1) + else: + valid_serial_nos.append(val) + + if qty and len(valid_serial_nos) != abs(qty): + msgprint("Please enter serial nos for " + + cstr(abs(qty)) + " quantity against item code: " + item_code, + raise_exception=1) + + return valid_serial_nos \ No newline at end of file From 418d580a8b722843fa5b586444084b1dc2da6fd9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 7 Jan 2013 19:35:20 +0530 Subject: [PATCH 02/18] fifo part of stock reconciliation --- .../stock_reconciliation.py | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 7db36e7ac1..02a75a8dfb 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -117,8 +117,11 @@ class DocType: 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): - return (qty * valuation_rate - previous_qty * previous_valuation_rate) \ - / flt(qty - previous_qty) + 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"] @@ -126,42 +129,69 @@ class DocType: for row_num, row in enumerate(data[1:]): row = webnotes._dict(zip(row_template, row)) - args = { + args = webnotes._dict({ "__islocal": 1, - "item_code": row[0], - "warehouse": row[1], + "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") - } + }) previous_sle = get_previous_sle(args) - qty_diff = _qty_diff(row[2], previous_sle) + qty_diff = _qty_diff(row.qty, previous_sle) - if get_valuation_method(row[0]) == "Moving Average": - rate_diff = _rate_diff(row[3], previous_sle) + if get_valuation_method(row.item_code) == "Moving Average": if qty_diff: - actual_qty = qty_diff, - if flt(previous_sle.valuation_rate): - incoming_rate = _get_incoming_rate(flt(row[2]), flt(row[3]), - flt(previous_sle.qty_after_transaction), - flt(previous_sle.valuation_rate)) - else: - incoming_rate = row[3] + incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate), + flt(previous_sle.qty_after_transaction), + flt(previous_sle.valuation_rate)) - webnotes.model_wrapper([args]).save() - elif rate_diff: + # 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 - pass + 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: # FIFO - # Make reverse entry + 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() + + - # make entry as per attachment - pass From 902e8609e5d9cc608e49c0f9f37cf73a5002239e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 8 Jan 2013 18:29:24 +0530 Subject: [PATCH 03/18] stock reco and utility function of stocks --- .../repost_stock_for_posting_time.py | 15 +- .../repost_stock_due_to_wrong_packing_list.py | 19 +- patches/september_2012/repost_stock.py | 13 +- stock/doctype/bin/bin.py | 335 ++++++------------ .../landed_cost_wizard/landed_cost_wizard.py | 10 +- .../stock_reconciliation.py | 173 ++++----- .../stock_uom_replace_utility.py | 6 +- stock/doctype/warehouse/warehouse.py | 16 +- stock/stock_ledger.py | 252 +++++++++++++ stock/utils.py | 8 + 10 files changed, 514 insertions(+), 333 deletions(-) create mode 100644 stock/stock_ledger.py diff --git a/patches/april_2012/repost_stock_for_posting_time.py b/patches/april_2012/repost_stock_for_posting_time.py index a1283a0327..2249ac190f 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 b20290287c..10d4f500b9 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 c6b6ce39f7..1fec9499ab 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 ea486cea0c..c9bd927649 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 d5abb84596..7067e52d0b 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 02a75a8dfb..b82a7f0dc1 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 0af37d6e9c..209bda5ff3 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 38ba3879d3..e65004bfe0 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 0000000000..d4475efa3a --- /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 28919c97fd..b3bf26af13 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" From 26d46556a483bc17f37f166958257e9e6754ec15 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 9 Jan 2013 15:23:05 +0530 Subject: [PATCH 04/18] first cut error fixes --- .../repost_stock_for_posting_time.py | 6 ++---- .../repost_stock_due_to_wrong_packing_list.py | 8 ++++---- patches/september_2012/repost_stock.py | 7 +------ stock/doctype/bin/bin.py | 8 ++++---- stock/doctype/stock_entry/stock_entry.py | 19 +++++++++---------- .../stock_reconciliation.py | 8 ++++---- .../stock_uom_replace_utility.py | 2 +- stock/doctype/warehouse/warehouse.py | 7 +------ stock/stock_ledger.py | 9 +++++---- stock/utils.py | 9 ++++----- 10 files changed, 35 insertions(+), 48 deletions(-) diff --git a/patches/april_2012/repost_stock_for_posting_time.py b/patches/april_2012/repost_stock_for_posting_time.py index 2249ac190f..d9cbbe5eb0 100644 --- a/patches/april_2012/repost_stock_for_posting_time.py +++ b/patches/april_2012/repost_stock_for_posting_time.py @@ -8,8 +8,6 @@ def execute(): 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' + "item_code": d.item_code, + "warehouse": d.warehouse, }) 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 10d4f500b9..81c6415644 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 @@ -82,10 +82,10 @@ def cleanup_wrong_sle(): webnotes.conn.sql("update `tabStock Ledger Entry` set is_cancelled = 'Yes' where name = %s", d[3]) create_comment(d[3]) update_entries_after({ - item_code: d[0], - warehouse: d[1], - posting_date: "2012-07-01", - posting_time: "12:05" + "item_code": d[0], + "warehouse": d[1], + "posting_date": "2012-07-01", + "posting_time": "12:05" }) def create_comment(dn): diff --git a/patches/september_2012/repost_stock.py b/patches/september_2012/repost_stock.py index 1fec9499ab..972070137a 100644 --- a/patches/september_2012/repost_stock.py +++ b/patches/september_2012/repost_stock.py @@ -22,12 +22,7 @@ def execute(): i=0 for d in res: try: - update_entries_after({ - item_code: d[0], - warehouse: d[1], - posting_date: "2000-01-01", - posting_time: "12:00" - }) + update_entries_after({ "item_code": d[0], "warehouse": d[1] }) except: pass i += 1 diff --git a/stock/doctype/bin/bin.py b/stock/doctype/bin/bin.py index c9bd927649..c473a6c1db 100644 --- a/stock/doctype/bin/bin.py +++ b/stock/doctype/bin/bin.py @@ -45,10 +45,10 @@ class DocType: if args.get("actual_qty"): # update valuation and qty after transaction for post dated entry 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") + "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): diff --git a/stock/doctype/stock_entry/stock_entry.py b/stock/doctype/stock_entry/stock_entry.py index ea44f1bbf0..e3cafa26d2 100644 --- a/stock/doctype/stock_entry/stock_entry.py +++ b/stock/doctype/stock_entry/stock_entry.py @@ -157,14 +157,13 @@ class DocType(TransactionBase): """get stock and incoming rate on posting date""" for d in getlist(self.doclist, 'mtn_details'): args = { - item_code: d.item_code, - warehouse: d.s_warehouse or d.t_warehouse, - posting_date: self.doc.posting_date, - posting_time: self.doc.posting_time, - qty: d.transfer_qty, - serial_no: d.serial_no, - bom_no: d.bom_no - + "item_code": d.item_code, + "warehouse": d.s_warehouse or d.t_warehouse, + "posting_date": self.doc.posting_date, + "posting_time": self.doc.posting_time, + "qty": d.transfer_qty, + "serial_no": d.serial_no, + "bom_no": d.bom_no } # get actual stock at source warehouse d.actual_qty = get_previous_sle(args).get("qty_after_transaction") or 0 @@ -308,8 +307,8 @@ class DocType(TransactionBase): import json args, actual_qty, in_rate = json.loads(args), 0, 0 args.update({ - posting_date: self.doc.posting_date, - posting_time: self.doc.posting_time + "posting_date": self.doc.posting_date, + "posting_time": self.doc.posting_time }) ret = { diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index b82a7f0dc1..021a833db8 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -205,10 +205,10 @@ class DocType(DocListController): # 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 + "item_code": entries.item_code, + "warehouse": entries.warehouse, + "posting_date": self.doc.posting_date, + "posting_time": self.doc.posting_time }) 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 209bda5ff3..9918010fd8 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 @@ -91,7 +91,7 @@ class DocType: if flt(self.doc.conversion_factor) != flt(1): wh = sql("select name from `tabWarehouse`") for w in wh: - update_entries_after({item_code: self.doc.item_code, warehouse: w[0]}) + 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 e65004bfe0..775f0d0302 100644 --- a/stock/doctype/warehouse/warehouse.py +++ b/stock/doctype/warehouse/warehouse.py @@ -121,12 +121,7 @@ class DocType: 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' - }) + update_entries_after({ "item_code": item_code, "warehouse": warehouse }) def repost_reserved_qty(self, bin): reserved_qty = webnotes.conn.sql(""" diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index d4475efa3a..bb02f8cd33 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -16,8 +16,9 @@ import webnotes from webnotes import msgprint, _ -from webnotes.utils import cint +from webnotes.utils import cint, flt, cstr from stock.utils import _msgprint, get_valuation_method +import json # future reposting @@ -67,7 +68,7 @@ def update_entries_after(args, verbose=1): qty_after_transaction += flt(sle.actual_qty) # get stock value - if serial_nos: + if sle.serial_nos: stock_value = qty_after_transaction * valuation_rate elif valuation_method == "Moving Average": stock_value = (qty_after_transaction > 0) and \ @@ -101,7 +102,7 @@ def get_sle_before_datetime(args): 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)"], + ["timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)"], "limit 1") return sle and sle[0] or webnotes._dict() @@ -109,7 +110,7 @@ def get_sle_before_datetime(args): 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)"]) + ["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""" diff --git a/stock/utils.py b/stock/utils.py index b3bf26af13..2c0eaefd30 100644 --- a/stock/utils.py +++ b/stock/utils.py @@ -77,12 +77,11 @@ def get_previous_sle(args): "sle": "name of reference Stock Ledger Entry" } """ - if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "12:00" + if not args.get("posting_date"): args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): args["posting_time"] = "12:00" + if not args.get("sle"): args["sle"] = "" - sle = sql(""" + sle = webnotes.conn.sql(""" select * from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s From 9514d170c2950a66f0c7baacf21ecf9feb82404d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 10 Jan 2013 10:40:37 +0530 Subject: [PATCH 05/18] stock reco testcases --- .../stock_reconciliation.py | 52 ++++++++----- .../test_stock_reconciliation.py | 78 ++++++++++++------- stock/stock_ledger.py | 29 ++++--- 3 files changed, 100 insertions(+), 59 deletions(-) diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 021a833db8..3e3ad205ad 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -20,12 +20,14 @@ import json from webnotes import msgprint, _ from webnotes.utils import cstr, flt from webnotes.model.controller import DocListController +from stock.stock_ledger import update_entries_after class DocType(DocListController): def validate(self): self.validate_data() def on_submit(self): + print "in stock reco" self.insert_stock_ledger_entries() def on_cancel(self): @@ -110,7 +112,6 @@ class DocType(DocListController): data = json.loads(self.doc.reconciliation_json) for row_num, row in enumerate(data[1:]): row = webnotes._dict(zip(row_template, row)) - previous_sle = get_previous_sle({ "item_code": row.item_code, "warehouse": row.warehouse, @@ -118,13 +119,20 @@ class DocType(DocListController): "posting_time": self.doc.posting_time }) + + 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 get_valuation_method(row.item_code) == "Moving Average": - self.sle_for_moving_avg(row, previous_sle) + self.sle_for_moving_avg(row, previous_sle, change_in_qty, change_in_rate) else: - self.sle_for_fifo(row, previous_sle) + self.sle_for_fifo(row, previous_sle, change_in_qty, change_in_rate) - def sle_for_moving_avg(self, row, previous_sle): + def sle_for_moving_avg(self, row, previous_sle, change_in_qty, change_in_rate): """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: @@ -132,12 +140,6 @@ class DocType(DocListController): 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), @@ -158,23 +160,27 @@ class DocType(DocListController): # -1 entry self.insert_entries({"actual_qty": -1}, row) - def sle_for_fifo(self, row, previous_sle): + def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate): """Insert Stock Ledger Entries for FIFO valuation""" - previous_stock_queue = json.loads(previous_sle.stock_queue) + previous_stock_queue = json.loads(previous_sle.stock_queue or "[]") - 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) + if change_in_qty: + 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) - + # Make reverse entry + qty = sum((flt(fifo_item[0]) for fifo_item in previous_stock_queue)) + self.insert_entries({"actual_qty": -1 * qty, + "incoming_rate": qty < 0 and row.valuation_rate or 0}, row) + + elif change_in_rate: + pass def insert_entries(self, opts, row): """Insert Stock Ledger Entries""" args = { + "doctype": "Stock Ledger Entry", "item_code": row.item_code, "warehouse": row.warehouse, "posting_date": self.doc.posting_date, @@ -185,8 +191,12 @@ class DocType(DocListController): "is_cancelled": "No" } args.update(opts) + print args + sle_wrapper = webnotes.model_wrapper([args]).insert() + + update_entries_after(args) - return webnotes.model_wrapper([args]).insert() + return sle_wrapper def delete_stock_ledger_entries(self): """ Delete Stock Ledger Entries related to this Stock Reconciliation diff --git a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index b967b8a54b..48f0019198 100644 --- a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -31,23 +31,45 @@ class TestStockReconciliation(unittest.TestCase): self.insert_test_data() def tearDown(self): - print "Message Log:", webnotes.message_log + # print "Message Log:", "\n--\n".join(webnotes.message_log) + # print "Debug Log:", "\n--\n".join(webnotes.debug_log) webnotes.conn.rollback() - def test_reco_for_fifo(self): - webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", "FIFO") - self.submit_stock_reconciliation("2012-12-26", "12:05", 50, 1000) + def test_reco_for_fifo(self): + # [[qty, valuation_rate, posting_date, posting_time]] + input_data = [ + # [50, 1000, "2012-12-26", "12:00", 50000], + # [5, 1000, "2012-12-26", "12:00", 5000], + # [15, 1000, "2012-12-26", "12:00", 15000], + # [25, 900, "2012-12-26", "12:00", 22500], + # [20, 500, "2012-12-26", "12:00", 10000], + # [50, 1000, "2013-01-01", "12:00", 50000], + # [5, 1000, "2013-01-01", "12:00", 5000], + ["", 800, "2012-12-26", "12:05", 12000], + # [20, "", "2012-12-26", "12:05", 16000] + ] + + for d in input_data: + self.insert_existing_sle("FIFO") + + reco = self.submit_stock_reconciliation(d[0], d[1], d[2], d[3]) - res = webnotes.conn.sql("""select stock_queue from `tabStock Ledger Entry` - where item_code = 'Android Jack D' and warehouse = 'Default Warehouse' - and voucher_no = 'RECO-001'""") + res = webnotes.conn.sql("""select stock_queue from `tabStock Ledger Entry` + where item_code = 'Android Jack D' and warehouse = 'Default Warehouse' + and posting_date = %s and posting_time = %s order by name desc limit 1""", + (d[2], d[3])) + + stock_value = sum([v[0]*v[1] for v in json.loads(res[0][0] or "[]")]) + self.assertEqual(stock_value, d[4]) + + self.tearDown() + self.setUp() + - self.assertEqual(res[0][0], [[50, 1000]]) - - def test_reco_for_moving_average(self): + def atest_reco_for_moving_average(self): webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", "Moving Average") - def submit_stock_reconciliation(self, posting_date, posting_time, qty, rate): + def submit_stock_reconciliation(self, qty, rate, posting_date, posting_time): return webnotes.model_wrapper([{ "doctype": "Stock Reconciliation", "name": "RECO-001", @@ -61,65 +83,69 @@ class TestStockReconciliation(unittest.TestCase): }]).submit() def insert_test_data(self): - # create item groups and items - insert_test_data("Item Group", - sort_fn=lambda ig: (ig[0].get('parent_item_group'), ig[0].get('name'))) - insert_test_data("Item") - # create default warehouse if not webnotes.conn.exists("Warehouse", "Default Warehouse"): webnotes.insert({"doctype": "Warehouse", "warehouse_name": "Default Warehouse", "warehouse_type": "Stores"}) - + # create UOM: Nos. if not webnotes.conn.exists("UOM", "Nos"): webnotes.insert({"doctype": "UOM", "uom_name": "Nos"}) + + # create item groups and items + insert_test_data("Item Group", + sort_fn=lambda ig: (ig[0].get('parent_item_group'), ig[0].get('name'))) + insert_test_data("Item") + + def insert_existing_sle(self, valuation_method): + webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", valuation_method) + webnotes.conn.set_default("allow_negative_stock", 1) existing_ledgers = [ { "doctype": "Stock Ledger Entry", "__islocal": 1, "voucher_type": "Stock Entry", "voucher_no": "TEST", "item_code": "Android Jack D", "warehouse": "Default Warehouse", - "posting_date": "2012-12-12", "posting_time": "01:00:00", + "posting_date": "2012-12-12", "posting_time": "01:00", "actual_qty": 20, "incoming_rate": 1000, "company": company }, { "doctype": "Stock Ledger Entry", "__islocal": 1, "voucher_type": "Stock Entry", "voucher_no": "TEST", "item_code": "Android Jack D", "warehouse": "Default Warehouse", - "posting_date": "2012-12-15", "posting_time": "02:00:00", + "posting_date": "2012-12-15", "posting_time": "02:00", "actual_qty": 10, "incoming_rate": 700, "company": company }, { "doctype": "Stock Ledger Entry", "__islocal": 1, "voucher_type": "Stock Entry", "voucher_no": "TEST", "item_code": "Android Jack D", "warehouse": "Default Warehouse", - "posting_date": "2012-12-25", "posting_time": "03:00:00", + "posting_date": "2012-12-25", "posting_time": "03:00", "actual_qty": -15, "company": company }, { "doctype": "Stock Ledger Entry", "__islocal": 1, "voucher_type": "Stock Entry", "voucher_no": "TEST", "item_code": "Android Jack D", "warehouse": "Default Warehouse", - "posting_date": "2012-12-31", "posting_time": "08:00:00", + "posting_date": "2012-12-31", "posting_time": "08:00", "actual_qty": -20, "company": company }, { "doctype": "Stock Ledger Entry", "__islocal": 1, "voucher_type": "Stock Entry", "voucher_no": "TEST", "item_code": "Android Jack D", "warehouse": "Default Warehouse", - "posting_date": "2013-01-05", "posting_time": "07:00:00", + "posting_date": "2013-01-05", "posting_time": "07:00", "actual_qty": 15, "incoming_rate": 1200, "company": company }, ] - pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' - and warehouse='Default Warehouse'""", as_dict=1)) + # pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' + # and warehouse='Default Warehouse'""", as_dict=1)) webnotes.get_obj("Stock Ledger").update_stock(existing_ledgers) - pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' - and warehouse='Default Warehouse'""", as_dict=1)) + # pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' + # and warehouse='Default Warehouse'""", as_dict=1)) \ No newline at end of file diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index bb02f8cd33..95c74d7e07 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -40,6 +40,7 @@ def update_entries_after(args, verbose=1): 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 "[]") + stock_value = 0.0 entries_to_fix = get_sle_after_datetime(previous_sle or \ {"item_code": args["item_code"], "warehouse": args["warehouse"]}) @@ -47,8 +48,7 @@ def update_entries_after(args, verbose=1): 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")): + if sle.serial_nos 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): @@ -75,7 +75,7 @@ def update_entries_after(args, verbose=1): (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, @@ -83,7 +83,7 @@ def update_entries_after(args, verbose=1): json.dumps(stock_queue), stock_value, incoming_rate, sle.name)) if _exceptions: - _raise_exceptions(args) + _raise_exceptions(args, verbose) # update bin webnotes.conn.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value=%s, @@ -103,16 +103,17 @@ def get_sle_before_datetime(args): """ sle = get_stock_ledger_entries(args, ["timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)"], - "limit 1") + "desc", "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)"]) + ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"], + "asc") -def get_stock_ledger_entries(args, conditions=None, limit=None): +def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None): """get stock ledger entries filtered by specific posting datetime conditions""" if not args.get("posting_date"): args["posting_date"] = "1900-01-01" @@ -124,10 +125,11 @@ def get_stock_ledger_entries(args, conditions=None, limit=None): and warehouse = %%(warehouse)s and ifnull(is_cancelled, 'No') = 'No' %(conditions)s - order by timestamp(posting_date, posting_time) desc, name desc + order by timestamp(posting_date, posting_time) %(order)s, name %(order)s %(limit)s""" % { "conditions": conditions and ("and " + " and ".join(conditions)) or "", - "limit": limit or "" + "limit": limit or "", + "order": order }, args, as_dict=1) def validate_negative_stock(qty_after_transaction, sle): @@ -202,7 +204,7 @@ def get_fifo_values(qty_after_transaction, sle, stock_queue): 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]) @@ -213,6 +215,9 @@ def get_fifo_values(qty_after_transaction, sle, stock_queue): incoming_cost = 0 qty_to_pop = abs(actual_qty) while qty_to_pop: + if not stock_queue: + stock_queue.append([0, 0]) + batch = stock_queue[0] if 0 < batch[0] <= qty_to_pop: @@ -233,10 +238,10 @@ def get_fifo_values(qty_after_transaction, sle, 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): +def _raise_exceptions(args, verbose=1): deficiency = min(e["diff"] for e in _exceptions) msg = """Negative stock error: Cannot complete this transaction because stock will start From b7d0dcdd149cfa8936cdd4083a4b26594469b147 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Thu, 10 Jan 2013 14:44:03 +0530 Subject: [PATCH 06/18] changed wn.downloadify to wn.tools.downloadify --- accounts/page/trial_balance/trial_balance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/page/trial_balance/trial_balance.js b/accounts/page/trial_balance/trial_balance.js index e5cfe5e405..dc87d583f8 100644 --- a/accounts/page/trial_balance/trial_balance.js +++ b/accounts/page/trial_balance/trial_balance.js @@ -61,7 +61,7 @@ wn.pages['trial-balance'].onload = function(wrapper) { return false; }); - wn.downloadify(data, ["Report Manager", "System Manager"], me); + wn.tools.downloadify(data, ["Report Manager", "System Manager"], me); return false; }) From 1b531866e0ae27e5cf966142fd5a3224ff6995d6 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Thu, 10 Jan 2013 19:29:51 +0530 Subject: [PATCH 07/18] fixes in stock reco, test case for serial no stock entry --- patches/january_2013/stock_reconciliation.py | 14 --- .../stock_reconciliation_patch.py | 35 +++++++ stock/doctype/serial_no/serial_no.py | 3 +- stock/doctype/serial_no/test_serial_no.py | 93 +++++++++++++++++++ stock/doctype/stock_entry/stock_entry.py | 3 +- stock/doctype/stock_ledger/stock_ledger.py | 27 ++---- .../stock_ledger_entry/stock_ledger_entry.py | 2 +- .../stock_reconciliation.js | 33 +++++-- .../stock_reconciliation.py | 69 +++++++++----- .../stock_reconciliation.txt | 27 ++++-- .../test_stock_reconciliation.py | 62 +++++++++---- stock/stock_ledger.py | 73 ++++++++++----- stock/utils.py | 34 +------ tests/data/item/nebula_8.txt | 31 +++++++ 14 files changed, 362 insertions(+), 144 deletions(-) delete mode 100644 patches/january_2013/stock_reconciliation.py create mode 100644 patches/january_2013/stock_reconciliation_patch.py create mode 100644 stock/doctype/serial_no/test_serial_no.py create mode 100644 tests/data/item/nebula_8.txt diff --git a/patches/january_2013/stock_reconciliation.py b/patches/january_2013/stock_reconciliation.py deleted file mode 100644 index e14044f7f0..0000000000 --- a/patches/january_2013/stock_reconciliation.py +++ /dev/null @@ -1,14 +0,0 @@ -import webnotes - -def execute(): - rename_fields() - -def rename_fields(): - webnotes.reload_doc("stock", "doctype", "stock_ledger_entry") - - args = [["Stock Ledger Entry", "bin_aqat", "qty_after_transaction"], - ["Stock Ledger Entry", "fcfs_stack", "stock_queue"]] - for doctype, old_fieldname, new_fieldname in args: - webnotes.conn.sql("""update `tab%s` set `%s`=`%s`""" % - (doctype, new_fieldname, old_fieldname)) - \ No newline at end of file diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py new file mode 100644 index 0000000000..75dab765e2 --- /dev/null +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -0,0 +1,35 @@ +import webnotes + +def execute(): + webnotes.reload_doc("stock", "doctype", "stock_ledger_entry") + + rename_fields() + store_stock_reco_json() + +def rename_fields(): + args = [["Stock Ledger Entry", "bin_aqat", "qty_after_transaction"], + ["Stock Ledger Entry", "fcfs_stack", "stock_queue"]] + for doctype, old_fieldname, new_fieldname in args: + webnotes.conn.sql("""update `tab%s` set `%s`=`%s`""" % + (doctype, new_fieldname, old_fieldname)) + +def store_stock_reco_json(): + import os + import conf + import json + from webnotes.utils.datautils import read_csv_content + base_path = os.path.dirname(os.path.abspath(conf.__file__)) + + for reco, file_list in webnotes.conn.sql("""select name, file_list + from `tabStock Reconciliation`"""): + if file_list: + file_list = file_list.split("\n") + stock_reco_file = file_list[0].split(",")[1] + stock_reco_file = os.path.join(base_path, "public", "files", stock_reco_file) + if os.path.exists(stock_reco_file): + with open(stock_reco_file, "r") as open_reco_file: + content = open_reco_file.read() + content = read_csv_content(content) + webnotes.conn.set_value("Stock Reconciliation", reco, "reconciliation_json", + json.dumps(content, separators=(',', ': '))) + \ No newline at end of file diff --git a/stock/doctype/serial_no/serial_no.py b/stock/doctype/serial_no/serial_no.py index 190b92be3f..00f2de7e40 100644 --- a/stock/doctype/serial_no/serial_no.py +++ b/stock/doctype/serial_no/serial_no.py @@ -82,7 +82,6 @@ class DocType(TransactionBase): self.make_stock_ledger_entry(1) webnotes.conn.set(self.doc, 'sle_exists', 1) - def make_stock_ledger_entry(self, qty): from webnotes.model.code import get_obj values = [{ @@ -103,7 +102,7 @@ class DocType(TransactionBase): 'batch_no' : '', 'serial_no' : self.doc.name }] - get_obj('Stock Ledger', 'Stock Ledger').update_stock(values) + get_obj('Stock Ledger').update_stock(values) # --------- diff --git a/stock/doctype/serial_no/test_serial_no.py b/stock/doctype/serial_no/test_serial_no.py new file mode 100644 index 0000000000..1398e68ae8 --- /dev/null +++ b/stock/doctype/serial_no/test_serial_no.py @@ -0,0 +1,93 @@ +# 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 . + + +from __future__ import unicode_literals +import unittest +import webnotes +from webnotes.tests import insert_test_data + +company = webnotes.conn.get_default("company") + +class TestSerialNo(unittest.TestCase): + def setUp(self): + webnotes.conn.begin() + self.insert_test_data() + + def tearDown(self): + # print "Message Log:", "\n--\n".join(webnotes.message_log) + # print "Debug Log:", "\n--\n".join(webnotes.debug_log) + webnotes.conn.rollback() + + def test_serialized_stock_entry(self): + data = [["2012-01-01", "01:00", "10001", 400, 400], + ["2012-01-01", "03:00", "10002", 500, 700], + ["2012-01-01", "04:00", "10003", 700, 700], + ["2012-01-01", "05:00", "10004", 1200, 800], + ["2012-01-01", "05:00", "10005", 800, 800], + ["2012-01-01", "02:00", "10006", 1200, 800], + ["2012-01-01", "06:00", "10007", 1500, 900]] + for d in data: + webnotes.model_wrapper([{ + "doctype": "Serial No", + "item_code": "Nebula 8", + "warehouse": "Default Warehouse", + "status": "In Store", + "sle_exists": 0, + "purchase_date": d[0], + "purchase_time": d[1], + "serial_no": d[2], + "purchase_rate": d[3], + "company": company, + }]).insert() + + for d in data: + res = webnotes.conn.sql("""select valuation_rate from `tabStock Ledger Entry` + where posting_date=%s and posting_time=%s and actual_qty=1 and serial_no=%s""", + (d[0], d[1], d[2])) + self.assertEquals(res[0][0], d[4]) + + print "deleted" + webnotes.delete_doc("Serial No", "10002") + + test_data = [["10001", 400, 400], + ["10003", 700, 766.666667], + ["10004", 1200, 875], + ["10005", 800, 860], + ["10006", 1200, 800], + ["10007", 1500, 966.666667]] + + for d in test_data: + res = webnotes.conn.sql("""select valuation_rate from `tabStock Ledger Entry` + where actual_qty=1 and serial_no=%s""", (d[0],)) + self.assertEquals(res[0][0], d[2]) + + def insert_test_data(self): + # create default warehouse + if not webnotes.conn.exists("Warehouse", "Default Warehouse"): + webnotes.insert({"doctype": "Warehouse", + "warehouse_name": "Default Warehouse", + "warehouse_type": "Stores"}) + + # create UOM: Nos. + if not webnotes.conn.exists("UOM", "Nos"): + webnotes.insert({"doctype": "UOM", "uom_name": "Nos"}) + + # create item groups and items + insert_test_data("Item Group", + sort_fn=lambda ig: (ig[0].get('parent_item_group'), ig[0].get('name'))) + + insert_test_data("Item") \ No newline at end of file diff --git a/stock/doctype/stock_entry/stock_entry.py b/stock/doctype/stock_entry/stock_entry.py index 975e12ea24..eaf796655d 100644 --- a/stock/doctype/stock_entry/stock_entry.py +++ b/stock/doctype/stock_entry/stock_entry.py @@ -23,7 +23,8 @@ from webnotes.model.doc import Document, addchild from webnotes.model.wrapper import getlist, copy_doclist from webnotes.model.code import get_obj from webnotes import msgprint, _ -from stock.utils import get_previous_sle, get_incoming_rate +from stock.utils import get_incoming_rate +from stock.stock_ledger import get_previous_sle sql = webnotes.conn.sql diff --git a/stock/doctype/stock_ledger/stock_ledger.py b/stock/doctype/stock_ledger/stock_ledger.py index 3231850d8c..10a905fad5 100644 --- a/stock/doctype/stock_ledger/stock_ledger.py +++ b/stock/doctype/stock_ledger/stock_ledger.py @@ -17,10 +17,9 @@ from __future__ import unicode_literals import webnotes -from webnotes.utils import add_days, cstr, flt, now, nowdate -from webnotes.model import db_exists +from webnotes.utils import add_days, cstr, flt, nowdate from webnotes.model.doc import Document -from webnotes.model.wrapper import getlist, copy_doclist +from webnotes.model.wrapper import getlist from webnotes.model.code import get_obj from webnotes import session, msgprint from stock.utils import get_valid_serial_nos @@ -196,7 +195,7 @@ class DocType: # get serial nos if v.get("serial_no"): serial_nos = get_valid_serial_nos(v["serial_no"], v['actual_qty'], v['item_code']) - + # reverse quantities for cancel if v.get('is_cancelled') == 'Yes': v['actual_qty'] = -flt(v['actual_qty']) @@ -216,19 +215,13 @@ class DocType: def make_entry(self, args): - sle = Document(doctype = 'Stock Ledger Entry') - for k in args.keys(): - # adds warehouse_type - if k == 'warehouse': - sle.fields['warehouse_type'] = webnotes.conn.get_value('Warehouse' , args[k], 'warehouse_type') - sle.fields[k] = args[k] - sle_obj = get_obj(doc=sle) - - # validate - sle_obj.validate() - sle.save(new = 1) - return sle.name - + args.update({"doctype": "Stock Ledger Entry"}) + if args.get("warehouse"): + args["warehouse_type"] = webnotes.conn.get_value('Warehouse' , args["warehouse"], + 'warehouse_type') + sle = webnotes.model_wrapper([args]).insert() + return sle.doc.name + def repost(self): """ Repost everything! diff --git a/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index ea9ac12778..9b73c6b341 100644 --- a/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -17,7 +17,7 @@ from __future__ import unicode_literals import webnotes -from webnotes.utils import cstr, cint, flt, cstr, getdate +from webnotes.utils import cint, flt, getdate sql = webnotes.conn.sql msgprint = webnotes.msgprint diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.js b/stock/doctype/stock_reconciliation/stock_reconciliation.js index 3758c59a88..1e64965a3f 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -20,15 +20,25 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ if(this.frm.doc.docstatus===0) { this.show_download_template(); this.show_upload(); + if(this.frm.doc.reconciliation_json) { + this.frm.set_intro("You can submit this Stock Reconciliation."); + } else { + this.frm.set_intro("Download the template, fill in data and \ + upload it."); + } + } + if(this.frm.doc.reconciliation_json) { + this.show_reconciliation_data(); + this.show_download_reconciliation_data(); } - if(this.frm.doc.reconciliation_json) this.show_reconciliation_data(); }, show_download_template: function() { var me = this; this.frm.add_custom_button("Download Template", function() { this.title = "Stock Reconcilation Template"; - wn.downloadify([["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, this); + wn.tools.downloadify([["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, + this); return false; }, "icon-download"); }, @@ -42,7 +52,7 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ wn.upload.make({ parent: $('#dit-upload-area'), args: { - method: 'stock.doctype.stock_reconciliation.stock_reconciliation.upload', + method: 'stock.doctype.stock_reconciliation.stock_reconciliation.upload' }, sample_url: "e.g. http://example.com/somefile.csv", callback: function(r) { @@ -53,6 +63,15 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ }); }, + show_download_reconciliation_data: function() { + var me = this; + this.frm.add_custom_button("Download Reconcilation Data", function() { + this.title = "Stock Reconcilation Data"; + wn.tools.downloadify(JSON.parse(me.frm.doc.reconciliation_json), null, this); + return false; + }, "icon-download"); + }, + show_reconciliation_data: function() { if(this.frm.doc.reconciliation_json) { var $wrapper = $(cur_frm.fields_dict.reconciliation_html.wrapper).empty(); @@ -62,8 +81,8 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ var result = ""; var _render = header - ? function(col) { return "" + col + "" } - : function(col) { return "" + col + "" }; + ? function(col) { return "" + col + ""; } + : function(col) { return "" + col + ""; }; $.each(data, function(i, row) { result += "" @@ -71,7 +90,7 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ + ""; }); return result; - } + }; var $reconciliation_table = $("
\ \ @@ -80,7 +99,7 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({
\
").appendTo($wrapper); } - }, + } }); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3e3ad205ad..725bb5ff13 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -27,7 +27,6 @@ class DocType(DocListController): self.validate_data() def on_submit(self): - print "in stock reco" self.insert_stock_ledger_entries() def on_cancel(self): @@ -94,7 +93,8 @@ class DocType(DocListController): if item.has_serial_no == "Yes": raise webnotes.ValidationError, (_("Serialized Item: '") + item_code + _("""' can not be managed using Stock Reconciliation.\ - You can add/delete Serial No directly, to modify stock of this item.""")) + You can add/delete Serial No directly, \ + to modify stock of this item.""")) # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) @@ -105,7 +105,8 @@ class DocType(DocListController): 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 + from stock.utils import get_valuation_method + from stock.stock_ledger import get_previous_sle row_template = ["item_code", "warehouse", "qty", "valuation_rate"] @@ -119,9 +120,8 @@ class DocType(DocListController): "posting_time": self.doc.posting_time }) - change_in_qty = row.qty != "" and \ - (flt(row.qty) != flt(previous_sle.get("qty_after_transaction"))) + (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"))) @@ -134,26 +134,33 @@ class DocType(DocListController): def sle_for_moving_avg(self, row, previous_sle, change_in_qty, change_in_rate): """Insert Stock Ledger Entries for Moving Average valuation""" - def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate): + def _get_incoming_rate(qty, valuation_rate, previous_qty, + previous_valuation_rate): if previous_valuation_rate == 0: - return valuation_rate + return flt(valuation_rate) else: + if valuation_rate == "": + valuation_rate = previous_valuation_rate + return (qty * valuation_rate - previous_qty * previous_valuation_rate) \ / flt(qty - previous_qty) - + if change_in_qty: + # if change in qty, irrespective of change in rate 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) + self.insert_entries({"actual_qty": change_in_qty, + "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, + # if no change in qty, but change in rate + # and positive actual stock before this reconciliation + 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) @@ -163,19 +170,37 @@ class DocType(DocListController): def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate): """Insert Stock Ledger Entries for FIFO valuation""" previous_stock_queue = json.loads(previous_sle.stock_queue or "[]") - - if change_in_qty: + previous_stock_qty = sum((batch[0] for batch in previous_stock_queue)) + previous_stock_value = sum((batch[0] * batch[1] for batch in \ + previous_stock_queue)) + + def _insert_entries(): 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) - + self.insert_entries({"actual_qty": row.qty, + "incoming_rate": flt(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, - "incoming_rate": qty < 0 and row.valuation_rate or 0}, row) + self.insert_entries({"actual_qty": -1 * previous_stock_qty, + "incoming_rate": previous_stock_qty < 0 and \ + flt(row.valuation_rate) or 0}, row) - elif change_in_rate: - pass + if change_in_qty: + if row.valuation_rate == "": + # dont want change in valuation + if previous_stock_qty > 0: + # set valuation_rate as previous valuation_rate + row.valuation_rate = \ + previous_stock_value / flt(previous_stock_qty) + + _insert_entries() + + elif change_in_rate and previous_stock_qty > 0: + # if no change in qty, but change in rate + # and positive actual stock before this reconciliation + + row.qty = previous_stock_qty + _insert_entries() def insert_entries(self, opts, row): """Insert Stock Ledger Entries""" @@ -191,7 +216,7 @@ class DocType(DocListController): "is_cancelled": "No" } args.update(opts) - print args + sle_wrapper = webnotes.model_wrapper([args]).insert() update_entries_after(args) diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.txt b/stock/doctype/stock_reconciliation/stock_reconciliation.txt index f5cf6235f7..272bf99f17 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.txt +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.txt @@ -2,20 +2,21 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2013-01-04 13:57:25", + "creation": "2013-01-09 11:24:35", "modified_by": "Administrator", - "modified": "2013-01-04 13:58:54" + "modified": "2013-01-10 19:26:28" }, { - "is_submittable": 1, "allow_attach": 0, + "is_submittable": 1, "allow_print": 1, "search_fields": "reconciliation_date", "module": "Stock", - "allow_email": 1, - "autoname": "SR/.######", - "name": "__common__", "doctype": "DocType", + "autoname": "SR/.######", + "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", + "allow_email": 1, + "name": "__common__", "max_attachments": 1, "allow_copy": 1 }, @@ -33,7 +34,6 @@ "read": 1, "doctype": "DocPerm", "parenttype": "DocType", - "role": "Material Manager", "parentfield": "permissions" }, { @@ -127,12 +127,13 @@ "hidden": 1 }, { - "amend": 1, + "amend": 0, "create": 1, "doctype": "DocPerm", "submit": 1, "write": 1, "cancel": 1, + "role": "Material Manager", "permlevel": 0 }, { @@ -142,6 +143,16 @@ "submit": 0, "write": 0, "cancel": 0, + "role": "Material Manager", "permlevel": 1 + }, + { + "create": 1, + "doctype": "DocPerm", + "submit": 1, + "write": 1, + "cancel": 1, + "role": "System Manager", + "permlevel": 0 } ] \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 48f0019198..fadc3b4139 100644 --- a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -19,8 +19,8 @@ from __future__ import unicode_literals import unittest import webnotes from webnotes.tests import insert_test_data +from webnotes.utils import flt import json -from accounts.utils import get_fiscal_year from pprint import pprint company = webnotes.conn.get_default("company") @@ -35,39 +35,67 @@ class TestStockReconciliation(unittest.TestCase): # print "Debug Log:", "\n--\n".join(webnotes.debug_log) webnotes.conn.rollback() - def test_reco_for_fifo(self): + def test_reco_for_fifo(self): # [[qty, valuation_rate, posting_date, posting_time]] input_data = [ - # [50, 1000, "2012-12-26", "12:00", 50000], - # [5, 1000, "2012-12-26", "12:00", 5000], - # [15, 1000, "2012-12-26", "12:00", 15000], - # [25, 900, "2012-12-26", "12:00", 22500], - # [20, 500, "2012-12-26", "12:00", 10000], - # [50, 1000, "2013-01-01", "12:00", 50000], - # [5, 1000, "2013-01-01", "12:00", 5000], - ["", 800, "2012-12-26", "12:05", 12000], - # [20, "", "2012-12-26", "12:05", 16000] + [50, 1000, "2012-12-26", "12:00", 50000], + [5, 1000, "2012-12-26", "12:00", 5000], + [15, 1000, "2012-12-26", "12:00", 15000], + [25, 900, "2012-12-26", "12:00", 22500], + [20, 500, "2012-12-26", "12:00", 10000], + [50, 1000, "2013-01-01", "12:00", 50000], + [5, 1000, "2013-01-01", "12:00", 5000], + ["", 1000, "2012-12-26", "12:05", 15000], + [20, "", "2012-12-26", "12:05", 16000], + [10, 2000, "2012-12-26", "12:10", 20000] ] for d in input_data: self.insert_existing_sle("FIFO") - reco = self.submit_stock_reconciliation(d[0], d[1], d[2], d[3]) + self.submit_stock_reconciliation(d[0], d[1], d[2], d[3]) - res = webnotes.conn.sql("""select stock_queue from `tabStock Ledger Entry` + res = webnotes.conn.sql("""select stock_value from `tabStock Ledger Entry` where item_code = 'Android Jack D' and warehouse = 'Default Warehouse' and posting_date = %s and posting_time = %s order by name desc limit 1""", (d[2], d[3])) - stock_value = sum([v[0]*v[1] for v in json.loads(res[0][0] or "[]")]) - self.assertEqual(stock_value, d[4]) + # stock_value = sum([v[0]*v[1] for v in json.loads(res and res[0][0] or "[]")]) + self.assertEqual(res and flt(res[0][0]) or 0, d[4]) self.tearDown() self.setUp() - def atest_reco_for_moving_average(self): - webnotes.conn.set_value("Item", "Android Jack D", "valuation_method", "Moving Average") + def test_reco_for_moving_average(self): + # [[qty, valuation_rate, posting_date, posting_time]] + input_data = [ + [50, 1000, "2012-12-26", "12:00", 50000], + [5, 1000, "2012-12-26", "12:00", 5000], + [15, 1000, "2012-12-26", "12:00", 15000], + [25, 900, "2012-12-26", "12:00", 22500], + [20, 500, "2012-12-26", "12:00", 10000], + [50, 1000, "2013-01-01", "12:00", 50000], + [5, 1000, "2013-01-01", "12:00", 5000], + ["", 1000, "2012-12-26", "12:05", 15000], + [20, "", "2012-12-26", "12:05", 18000], + [10, 2000, "2012-12-26", "12:10", 20000] + ] + + for d in input_data: + self.insert_existing_sle("Moving Average") + + self.submit_stock_reconciliation(d[0], d[1], d[2], d[3]) + + res = webnotes.conn.sql("""select stock_value from `tabStock Ledger Entry` + where item_code = 'Android Jack D' and warehouse = 'Default Warehouse' + and posting_date = %s and posting_time = %s order by name desc limit 1""", + (d[2], d[3])) + + self.assertEqual(res and flt(res[0][0], 4) or 0, d[4]) + + self.tearDown() + self.setUp() def submit_stock_reconciliation(self, qty, rate, posting_date, posting_time): return webnotes.model_wrapper([{ diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index 95c74d7e07..d9629d0f1b 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -15,9 +15,9 @@ # along with this program. If not, see . import webnotes -from webnotes import msgprint, _ +from webnotes import msgprint from webnotes.utils import cint, flt, cstr -from stock.utils import _msgprint, get_valuation_method +from stock.utils import get_valuation_method import json # future reposting @@ -48,14 +48,14 @@ def update_entries_after(args, verbose=1): valuation_method = get_valuation_method(args["item_code"]) for sle in entries_to_fix: - if sle.serial_nos or not cint(webnotes.conn.get_default("allow_negative_stock")): + if sle.serial_no 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: + if sle.serial_no: valuation_rate, incoming_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate) elif valuation_method == "Moving Average": @@ -64,11 +64,11 @@ def update_entries_after(args, verbose=1): 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 sle.serial_nos: + if sle.serial_no: stock_value = qty_after_transaction * valuation_rate elif valuation_method == "Moving Average": stock_value = (qty_after_transaction > 0) and \ @@ -76,17 +76,21 @@ def update_entries_after(args, verbose=1): else: stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue)) + # print sle.posting_date, qty_after_transaction, incoming_rate, valuation_rate + # 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, + 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, verbose) # update bin - webnotes.conn.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value=%s, + 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"])) @@ -151,7 +155,7 @@ def validate_negative_stock(qty_after_transaction, sle): 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") + serial_no = cstr(sle.serial_no).split("\n") if incoming_rate < 0: # wrong incoming rate @@ -160,8 +164,8 @@ def get_serialized_values(qty_after_transaction, sle, valuation_rate): # 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]) + from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_no))), + tuple(serial_no))[0][0]) if incoming_rate and not valuation_rate: valuation_rate = incoming_rate @@ -173,29 +177,31 @@ def get_serialized_values(qty_after_transaction, sle, valuation_rate): # 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: + if not incoming_rate: # 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 + elif qty_after_transaction < 0: + # if negative stock, take current valuation rate as incoming rate + valuation_rate = incoming_rate + 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: + + if 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 - + + # NOTE: val_rate is same as previous entry if new stock value is negative + return valuation_rate, incoming_rate def get_fifo_values(qty_after_transaction, sle, stock_queue): @@ -255,4 +261,25 @@ def _raise_exceptions(args, verbose=1): if verbose: msgprint(msg, raise_exception=1) else: - raise webnotes.ValidationError, msg \ No newline at end of file + raise webnotes.ValidationError, msg + +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("sle"): args["sle"] = "" + + sle = get_stock_ledger_entries(args, ["name != %(sle)s", + "timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s)"], + "desc", "limit 1") + return sle and sle[0] or {} \ No newline at end of file diff --git a/stock/utils.py b/stock/utils.py index 2c0eaefd30..a65406beb0 100644 --- a/stock/utils.py +++ b/stock/utils.py @@ -17,7 +17,7 @@ import webnotes from webnotes import msgprint, _ import json -from webnotes.utils import flt +from webnotes.utils import flt, cstr def validate_end_of_life(item_code, end_of_life=None, verbose=1): if not end_of_life: @@ -63,39 +63,9 @@ def _msgprint(msg, verbose): else: raise webnotes.ValidationError, msg -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" - if not args.get("posting_time"): args["posting_time"] = "12:00" - if not args.get("sle"): args["sle"] = "" - - sle = webnotes.conn.sql(""" - select * from `tabStock Ledger Entry` - where item_code = %(item_code)s - and warehouse = %(warehouse)s - and ifnull(is_cancelled, 'No') = 'No' - and name != %(sle)s - and timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s) - order by timestamp(posting_date, posting_time) desc, name desc - limit 1 - """, args, as_dict=1) - - return sle and sle[0] or {} - def get_incoming_rate(args): """Get Incoming Rate based on valuation method""" + from stock.stock_ledger import get_previous_sle in_rate = 0 if args.get("serial_no"): diff --git a/tests/data/item/nebula_8.txt b/tests/data/item/nebula_8.txt new file mode 100644 index 0000000000..a666379979 --- /dev/null +++ b/tests/data/item/nebula_8.txt @@ -0,0 +1,31 @@ +[ + { + "owner": "Administrator", + "docstatus": 0, + "creation": "2012-08-26 11:32:02", + "modified_by": "Administrator", + "modified": "2012-08-26 11:32:02" + }, + { + "is_service_item": "No", + "description": "Nebula 8", + "item_code": "Nebula 8", + "is_stock_item": "Yes", + "inspection_required": "No", + "is_purchase_item": "No", + "name": "__common__", + "item_name": "Nebula 8", + "item_group": "Small Tablets", + "doctype": "Item", + "is_sales_item": "Yes", + "is_sub_contracted_item": "Yes", + "stock_uom": "Nos", + "has_batch_no": "No", + "has_serial_no": "Yes", + "default_warehouse": "Default Warehouse" + }, + { + "name": "Nebula 8", + "doctype": "Item" + } +] \ No newline at end of file From 4dc7caadf2cacd9d6ad06b9521dc55e77b42ce2a Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Fri, 11 Jan 2013 11:44:49 +0530 Subject: [PATCH 08/18] use for update lock when updating stock entries --- stock/stock_ledger.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index d9629d0f1b..f88ea5d0f6 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -43,7 +43,7 @@ def update_entries_after(args, verbose=1): stock_value = 0.0 entries_to_fix = get_sle_after_datetime(previous_sle or \ - {"item_code": args["item_code"], "warehouse": args["warehouse"]}) + {"item_code": args["item_code"], "warehouse": args["warehouse"]}, for_update=True) valuation_method = get_valuation_method(args["item_code"]) @@ -95,7 +95,7 @@ def update_entries_after(args, verbose=1): 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): +def get_sle_before_datetime(args, for_update=False): """ get previous stock ledger entry before current time-bucket @@ -107,17 +107,18 @@ def get_sle_before_datetime(args): """ sle = get_stock_ledger_entries(args, ["timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)"], - "desc", "limit 1") + "desc", "limit 1", for_update=for_update) return sle and sle[0] or webnotes._dict() -def get_sle_after_datetime(args): +def get_sle_after_datetime(args, for_update=False): """get Stock Ledger Entries after a particular datetime, for reposting""" + # NOTE: using for update of return get_stock_ledger_entries(args, ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"], - "asc") + "asc", for_update=for_update) -def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None): +def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None, for_update=False): """get stock ledger entries filtered by specific posting datetime conditions""" if not args.get("posting_date"): args["posting_date"] = "1900-01-01" @@ -130,9 +131,10 @@ def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None): and ifnull(is_cancelled, 'No') = 'No' %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, name %(order)s - %(limit)s""" % { + %(limit)s %(for_update)s""" % { "conditions": conditions and ("and " + " and ".join(conditions)) or "", "limit": limit or "", + "for_update": for_update and "for update" or "", "order": order }, args, as_dict=1) @@ -263,7 +265,7 @@ def _raise_exceptions(args, verbose=1): else: raise webnotes.ValidationError, msg -def get_previous_sle(args): +def get_previous_sle(args, for_update=False): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -281,5 +283,5 @@ def get_previous_sle(args): sle = get_stock_ledger_entries(args, ["name != %(sle)s", "timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s)"], - "desc", "limit 1") + "desc", "limit 1", for_update=for_update) return sle and sle[0] or {} \ No newline at end of file From ac53b119694bf6aa3c5aa1d5020ad6084dafa931 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 11 Jan 2013 19:25:46 +0530 Subject: [PATCH 09/18] stock reco fixes with testcases --- .../stock_reconciliation_patch.py | 21 ++- public/js/stock_controller.js | 32 ++++ stock/doctype/bin/bin.py | 154 ++++-------------- stock/doctype/stock_entry/stock_entry.js | 18 +- stock/doctype/stock_entry/stock_entry.txt | 52 ++++-- .../stock_entry_detail/stock_entry_detail.txt | 7 +- .../stock_reconciliation.js | 54 ++++-- .../stock_reconciliation.py | 45 +++-- .../stock_reconciliation.txt | 58 ++----- .../test_stock_reconciliation.py | 47 +++--- stock/doctype/warehouse/warehouse.py | 16 +- stock/stock_ledger.py | 10 +- 12 files changed, 245 insertions(+), 269 deletions(-) create mode 100644 public/js/stock_controller.js diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 75dab765e2..0bfcc988f0 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -4,6 +4,7 @@ def execute(): webnotes.reload_doc("stock", "doctype", "stock_ledger_entry") rename_fields() + move_remarks_to_comments() store_stock_reco_json() def rename_fields(): @@ -13,6 +14,21 @@ def rename_fields(): webnotes.conn.sql("""update `tab%s` set `%s`=`%s`""" % (doctype, new_fieldname, old_fieldname)) +def move_remarks_to_comments(): + from webnotes.utils import get_fullname + result = webnotes.conn.sql("""select name, remark, modified_by from `tabStock Reconciliation` + where ifnull(remark, '')!=''""") + fullname_map = {} + for reco, remark, modified_by in result: + webnotes.model_wrapper([{ + "doctype": "Comment", + "comment": remark, + "comment_by": modified_by, + "comment_by_fullname": fullname_map.setdefault(modified_by, get_fullname(modified_by)), + "comment_doctype": "Stock Reconciliation", + "comment_docname": reco + }]).insert() + def store_stock_reco_json(): import os import conf @@ -30,6 +46,7 @@ def store_stock_reco_json(): with open(stock_reco_file, "r") as open_reco_file: content = open_reco_file.read() content = read_csv_content(content) - webnotes.conn.set_value("Stock Reconciliation", reco, "reconciliation_json", - json.dumps(content, separators=(',', ': '))) + reconciliation_json = json.dumps(content, separators=(',', ': ')) + webnotes.conn.sql("""update `tabStock Reconciliation` + set reconciliation_json=%s where name=%s""", (reconciliation_json, name)) \ No newline at end of file diff --git a/public/js/stock_controller.js b/public/js/stock_controller.js new file mode 100644 index 0000000000..d3511e1c03 --- /dev/null +++ b/public/js/stock_controller.js @@ -0,0 +1,32 @@ +// 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 . + +wn.provide("erpnext.stock"); + +erpnext.stock.StockController = erpnext.utils.Controller.extend({ + show_stock_ledger: function() { + var me = this; + this.frm.add_custom_button("Show Stock Ledger", function() { + var args = { + voucher_no: cur_frm.doc.name, + from_date: wn.datetime.str_to_user(cur_frm.doc.posting_date), + to_date: wn.datetime.str_to_user(cur_frm.doc.posting_date) + }; + wn.set_route('stock-ledger', + $.map(args, function(val, key) { return key+"="+val; }).join("&&")); + }, "icon-bar-chart"); + } +}); \ No newline at end of file diff --git a/stock/doctype/bin/bin.py b/stock/doctype/bin/bin.py index c473a6c1db..19ce8f9e51 100644 --- a/stock/doctype/bin/bin.py +++ b/stock/doctype/bin/bin.py @@ -31,18 +31,34 @@ class DocType: self.doc = doc self.doclist = doclist + def validate(self): + if not self.doc.stock_uom: + self.doc.stock_uom = webnotes.conn.get_value('Item', self.doc.item_code, 'stock_uom') + + if not self.doc.warehouse_type: + self.doc.warehouse_type = webnotes.conn.get_value("Warehouse", self.doc.warehouse, + "warehouse_type") + + self.validate_mandatory() + + self.doc.projected_qty = flt(self.doc.actual_qty) + flt(self.doc.ordered_qty) + \ + flt(self.doc.indented_qty) + flt(self.doc.planned_qty) - flt(self.doc.reserved_qty) + + def validate_mandatory(self): + qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty'] + for f in qf: + if (not self.doc.fields.has_key(f)) or (not self.doc.fields[f]): + self.doc.fields[f] = 0.0 + def update_stock(self, args): - from stock.stock_ledger import update_entries_after - if not args.get("posting_date"): - posting_date = nowdate() - self.update_qty(args) - if (flt(args.get("actual_qty")) < 0 or flt(args.get("reserved_qty")) > 0) \ - and args.get("is_cancelled") == 'No' and args.get("is_amended")=='No': - self.reorder_item(args.get("voucher_type"), args.get("voucher_no")) - if args.get("actual_qty"): + from stock.stock_ledger import update_entries_after + + if not args.get("posting_date"): + posting_date = nowdate() + # update valuation and qty after transaction for post dated entry update_entries_after({ "item_code": self.doc.item_code, @@ -53,8 +69,8 @@ class DocType: 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)) - self.doc.ordered_qty = flt(self.doc.ordered_qty) + flt(args.get("ordered_qty", 0)) + self.doc.actual_qty = flt(self.doc.actual_qty) + flt(args.get("actual_qty")) + self.doc.ordered_qty = flt(self.doc.ordered_qty) + flt(args.get("ordered_qty")) self.doc.reserved_qty = flt(self.doc.reserved_qty) + flt(args.get("reserved_qty")) self.doc.indented_qty = flt(self.doc.indented_qty) + flt(args.get("indented_qty")) self.doc.planned_qty = flt(self.doc.planned_qty) + flt(args.get("planned_qty")) @@ -63,6 +79,10 @@ class DocType: flt(self.doc.indented_qty) + flt(self.doc.planned_qty) - flt(self.doc.reserved_qty) self.doc.save() + + if (flt(args.get("actual_qty")) < 0 or flt(args.get("reserved_qty")) > 0) \ + and args.get("is_cancelled") == 'No' and args.get("is_amended")=='No': + self.reorder_item(args.get("voucher_type"), args.get("voucher_no")) def get_first_sle(self): sle = sql(""" @@ -75,111 +95,6 @@ class DocType: """, (self.doc.item_code, self.doc.warehouse), as_dict=1) return sle and sle[0] or None - - - # 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 - - # 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 reorder_item(self,doc_type,doc_name): """ Reorder item if stock reaches reorder level""" @@ -246,12 +161,3 @@ class DocType: msg="""A Purchase Request has been raised for item %s: %s on %s """ % (doc_type, doc_name, nowdate()) sendmail(email_list, subject='Auto Purchase Request Generation Notification', msg = msg) - - def validate(self): - self.validate_mandatory() - - def validate_mandatory(self): - qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty'] - for f in qf: - if (not self.doc.fields.has_key(f)) or (not self.doc.fields[f]): - self.doc.fields[f] = 0.0 diff --git a/stock/doctype/stock_entry/stock_entry.js b/stock/doctype/stock_entry/stock_entry.js index a6d233e258..bb55622453 100644 --- a/stock/doctype/stock_entry/stock_entry.js +++ b/stock/doctype/stock_entry/stock_entry.js @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +wn.require("public/app/js/stock_controller.js"); wn.provide("erpnext.stock"); -erpnext.stock.StockEntry = erpnext.utils.Controller.extend({ +erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ onload_post_render: function() { this._super(); if(this.frm.doc.__islocal && (this.frm.doc.production_order || this.frm.doc.bom_no) @@ -30,8 +31,9 @@ erpnext.stock.StockEntry = erpnext.utils.Controller.extend({ this._super(); this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); - if (this.frm.doc.docstatus==1) this.frm.add_custom_button("Show Stock Ledger", - this.show_stock_ledger) + if (this.frm.doc.docstatus==1) { + this.show_stock_ledger(); + } }, on_submit: function() { @@ -108,16 +110,6 @@ cur_frm.cscript.toggle_related_fields = function(doc) { } } -cur_frm.cscript.show_stock_ledger = function() { - var args = { - voucher_no: cur_frm.doc.name, - from_date: wn.datetime.str_to_user(cur_frm.doc.posting_date), - to_date: wn.datetime.str_to_user(cur_frm.doc.posting_date) - }; - wn.set_route('stock-ledger', - $.map(args, function(val, key) { return key+"="+val; }).join("&&")); -} - cur_frm.cscript.delivery_note_no = function(doc,cdt,cdn){ if(doc.delivery_note_no) get_server_fields('get_cust_values','','',doc,cdt,cdn,1); } diff --git a/stock/doctype/stock_entry/stock_entry.txt b/stock/doctype/stock_entry/stock_entry.txt index d3b39c353e..76a8d42da4 100644 --- a/stock/doctype/stock_entry/stock_entry.txt +++ b/stock/doctype/stock_entry/stock_entry.txt @@ -2,27 +2,27 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2012-12-19 12:29:07", + "creation": "2012-12-24 18:32:32", "modified_by": "Administrator", - "modified": "2012-12-19 18:09:15" + "modified": "2013-01-11 11:54:51" }, { - "is_submittable": 1, "in_create": 0, + "is_submittable": 1, "allow_print": 0, "search_fields": "transfer_date, from_warehouse, to_warehouse, purpose, remarks", "module": "Stock", - "autoname": "naming_series:", + "doctype": "DocType", "read_only_onload": 0, "in_dialog": 0, + "issingle": 0, "allow_attach": 0, "read_only": 0, "allow_email": 0, "hide_heading": 0, - "issingle": 0, + "autoname": "naming_series:", "name": "__common__", "allow_rename": 0, - "doctype": "DocType", "max_attachments": 0, "hide_toolbar": 0, "allow_copy": 0 @@ -47,6 +47,7 @@ "doctype": "DocType" }, { + "print_width": "50%", "oldfieldtype": "Column Break", "doctype": "DocField", "width": "50%", @@ -93,6 +94,7 @@ "in_filter": 1 }, { + "print_width": "50%", "oldfieldtype": "Column Break", "doctype": "DocField", "width": "50%", @@ -146,7 +148,7 @@ }, { "print_hide": 1, - "no_copy": 0, + "no_copy": 1, "oldfieldtype": "Link", "allow_on_submit": 0, "doctype": "DocField", @@ -170,7 +172,7 @@ }, { "print_hide": 1, - "no_copy": 0, + "no_copy": 1, "oldfieldtype": "Link", "allow_on_submit": 0, "doctype": "DocField", @@ -279,7 +281,7 @@ { "print_hide": 1, "depends_on": "eval:doc.purpose==\"Sales Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 1, "allow_on_submit": 0, "doctype": "DocField", @@ -298,7 +300,7 @@ { "print_hide": 1, "depends_on": "eval:doc.purpose==\"Purchase Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 1, "allow_on_submit": 0, "doctype": "DocField", @@ -349,6 +351,7 @@ }, { "print_hide": 1, + "no_copy": 1, "depends_on": "eval:doc.purpose==\"Sales Return\"", "doctype": "DocField", "label": "Sales Invoice No", @@ -369,7 +372,7 @@ { "print_hide": 1, "depends_on": "eval:doc.purpose==\"Purchase Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -388,7 +391,7 @@ { "print_hide": 0, "depends_on": "eval:doc.purpose==\"Purchase Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -406,7 +409,7 @@ { "print_hide": 0, "depends_on": "eval:doc.purpose==\"Purchase Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -424,7 +427,7 @@ { "print_hide": 1, "depends_on": "eval:doc.purpose==\"Sales Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -443,7 +446,7 @@ { "print_hide": 0, "depends_on": "eval:doc.purpose==\"Sales Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -461,7 +464,7 @@ { "print_hide": 0, "depends_on": "eval:doc.purpose==\"Sales Return\"", - "no_copy": 0, + "no_copy": 1, "search_index": 0, "allow_on_submit": 0, "doctype": "DocField", @@ -485,6 +488,7 @@ "permlevel": 0 }, { + "print_width": "50%", "doctype": "DocField", "width": "50%", "fieldname": "col4", @@ -539,6 +543,7 @@ "in_filter": 1 }, { + "print_width": "50%", "doctype": "DocField", "width": "50%", "fieldname": "col5", @@ -601,16 +606,23 @@ "permlevel": 1 }, { + "amend": 0, "create": 0, "doctype": "DocPerm", + "submit": 0, "write": 1, "role": "Manufacturing User", + "cancel": 0, "permlevel": 2 }, { + "amend": 0, + "create": 0, "doctype": "DocPerm", + "submit": 0, "write": 1, "role": "Manufacturing Manager", + "cancel": 0, "permlevel": 2 }, { @@ -624,8 +636,12 @@ "permlevel": 0 }, { + "amend": 0, + "create": 0, "doctype": "DocPerm", + "submit": 0, "role": "Manufacturing User", + "cancel": 0, "permlevel": 1 }, { @@ -639,8 +655,12 @@ "permlevel": 0 }, { + "amend": 0, + "create": 0, "doctype": "DocPerm", + "submit": 0, "role": "Manufacturing Manager", + "cancel": 0, "permlevel": 1 }, { diff --git a/stock/doctype/stock_entry_detail/stock_entry_detail.txt b/stock/doctype/stock_entry_detail/stock_entry_detail.txt index 6926c9a7cf..a6b9521959 100644 --- a/stock/doctype/stock_entry_detail/stock_entry_detail.txt +++ b/stock/doctype/stock_entry_detail/stock_entry_detail.txt @@ -2,9 +2,9 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2012-12-18 13:47:41", + "creation": "2012-12-20 14:31:18", "modified_by": "Administrator", - "modified": "2012-12-18 17:08:52" + "modified": "2013-01-11 11:59:10" }, { "istable": 1, @@ -26,6 +26,7 @@ "doctype": "DocType" }, { + "no_copy": 1, "oldfieldtype": "Link", "doctype": "DocField", "label": "Source Warehouse", @@ -37,6 +38,7 @@ "in_filter": 1 }, { + "no_copy": 1, "oldfieldtype": "Link", "doctype": "DocField", "label": "Target Warehouse", @@ -61,6 +63,7 @@ "in_filter": 1 }, { + "print_width": "300px", "oldfieldtype": "Text", "doctype": "DocField", "label": "Description", diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.js b/stock/doctype/stock_reconciliation/stock_reconciliation.js index 1e64965a3f..62bc69fcc9 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -13,9 +13,11 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . + +wn.require("public/app/js/stock_controller.js"); wn.provide("erpnext.stock"); -erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ +erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({ refresh: function() { if(this.frm.doc.docstatus===0) { this.show_download_template(); @@ -23,22 +25,37 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ if(this.frm.doc.reconciliation_json) { this.frm.set_intro("You can submit this Stock Reconciliation."); } else { - this.frm.set_intro("Download the template, fill in data and \ - upload it."); + this.frm.set_intro("Download the Template, fill appropriate data and \ + attach the modified file."); } + } else if(this.frm.doc.docstatus == 1) { + this.frm.set_intro("Cancelling this Stock Reconciliation will nullify it's effect."); + this.show_stock_ledger(); + } else { + this.frm.set_intro(""); } - if(this.frm.doc.reconciliation_json) { - this.show_reconciliation_data(); - this.show_download_reconciliation_data(); - } + this.show_reconciliation_data(); + this.show_download_reconciliation_data(); }, show_download_template: function() { var me = this; this.frm.add_custom_button("Download Template", function() { this.title = "Stock Reconcilation Template"; - wn.tools.downloadify([["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, - this); + wn.tools.downloadify([["Stock Reconciliation"], + ["----"], + ["Stock Reconciliation can be used to update the stock on a particular date," + + " usually as per physical inventory."], + ["When submitted, the system creates difference entries" + + " to set the given stock and valuation on this date."], + ["It can also be used to create opening stock entries and to fix stock value."], + ["----"], + ["Notes:"], + ["Item Code and Warehouse should already exist."], + ["You can update either Quantity or Valuation Rate or both."], + ["If no change in either Quantity or Valuation Rate, leave the cell blank."], + ["----"], + ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, this); return false; }, "icon-download"); }, @@ -59,22 +76,25 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ $wrapper.find(".dit-progress-area").toggle(false); me.frm.set_value("reconciliation_json", JSON.stringify(r)); me.show_reconciliation_data(); + me.frm.save(); } }); }, show_download_reconciliation_data: function() { var me = this; - this.frm.add_custom_button("Download Reconcilation Data", function() { - this.title = "Stock Reconcilation Data"; - wn.tools.downloadify(JSON.parse(me.frm.doc.reconciliation_json), null, this); - return false; - }, "icon-download"); + if(this.frm.doc.reconciliation_json) { + this.frm.add_custom_button("Download Reconcilation Data", function() { + this.title = "Stock Reconcilation Data"; + wn.tools.downloadify(JSON.parse(me.frm.doc.reconciliation_json), null, this); + return false; + }, "icon-download"); + } }, show_reconciliation_data: function() { + var $wrapper = $(cur_frm.fields_dict.reconciliation_html.wrapper).empty(); if(this.frm.doc.reconciliation_json) { - var $wrapper = $(cur_frm.fields_dict.reconciliation_html.wrapper).empty(); var reconciliation_data = JSON.parse(this.frm.doc.reconciliation_json); var _make = function(data, header) { @@ -92,14 +112,14 @@ erpnext.stock.StockReconciliation = erpnext.utils.Controller.extend({ return result; }; - var $reconciliation_table = $("
\ + var $reconciliation_table = $("
\ \ " + _make([reconciliation_data[0]], true) + "\ " + _make(reconciliation_data.splice(1)) + "\
\
").appendTo($wrapper); } - } + }, }); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.py b/stock/doctype/stock_reconciliation/stock_reconciliation.py index 725bb5ff13..3a8ffcde07 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -23,6 +23,9 @@ from webnotes.model.controller import DocListController from stock.stock_ledger import update_entries_after class DocType(DocListController): + def setup(self): + self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"] + def validate(self): self.validate_data() @@ -34,17 +37,22 @@ class DocType(DocListController): def validate_data(self): data = json.loads(self.doc.reconciliation_json) - if data[0] != ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]: + if self.head_row not in data: msgprint(_("""Hey! You seem to be using the wrong template. \ Click on 'Download Template' button to get the correct template."""), raise_exception=1) + + # remove the help part and save the json + if data.index(self.head_row) != 0: + data = data[data.index(self.head_row):] + self.doc.reconciliation_json = json.dumps(data) def _get_msg(row_num, msg): return _("Row # ") + ("%d: " % (row_num+2)) + _(msg) self.validation_messages = [] item_warehouse_combinations = [] - for row_num, row in enumerate(data[1:]): + for row_num, row in enumerate(data[data.index(self.head_row)+1:]): # find duplicates if [row[0], row[1]] in item_warehouse_combinations: self.validation_messages.append(_get_msg(row_num, "Duplicate entry")) @@ -111,7 +119,7 @@ class DocType(DocListController): row_template = ["item_code", "warehouse", "qty", "valuation_rate"] data = json.loads(self.doc.reconciliation_json) - for row_num, row in enumerate(data[1:]): + for row_num, row in enumerate(data[data.index(self.head_row)+1:]): row = webnotes._dict(zip(row_template, row)) previous_sle = get_previous_sle({ "item_code": row.item_code, @@ -148,18 +156,19 @@ class DocType(DocListController): if change_in_qty: # if change in qty, irrespective of change in rate incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate), - flt(previous_sle.qty_after_transaction), - flt(previous_sle.valuation_rate)) + flt(previous_sle.get("qty_after_transaction")), + flt(previous_sle.get("valuation_rate"))) self.insert_entries({"actual_qty": change_in_qty, "incoming_rate": incoming_rate}, row) - elif change_in_rate and previous_sle.qty_after_transaction >= 0: + elif change_in_rate and flt(previous_sle.get("qty_after_transaction")) >= 0: # if no change in qty, but change in rate # and positive actual stock before this reconciliation - 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)) + incoming_rate = _get_incoming_rate( + flt(previous_sle.get("qty_after_transaction"))+1, flt(row.valuation_rate), + flt(previous_sle.get("qty_after_transaction")), + flt(previous_sle.get("valuation_rate"))) # +1 entry self.insert_entries({"actual_qty": 1, "incoming_rate": incoming_rate}, row) @@ -169,7 +178,7 @@ class DocType(DocListController): def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate): """Insert Stock Ledger Entries for FIFO valuation""" - previous_stock_queue = json.loads(previous_sle.stock_queue or "[]") + previous_stock_queue = json.loads(previous_sle.get("stock_queue") or "[]") previous_stock_qty = sum((batch[0] for batch in previous_stock_queue)) previous_stock_value = sum((batch[0] * batch[1] for batch in \ previous_stock_queue)) @@ -181,9 +190,11 @@ class DocType(DocListController): "incoming_rate": flt(row.valuation_rate)}, row) # Make reverse entry - self.insert_entries({"actual_qty": -1 * previous_stock_qty, - "incoming_rate": previous_stock_qty < 0 and \ - flt(row.valuation_rate) or 0}, row) + if previous_stock_qty: + self.insert_entries({"actual_qty": -1 * previous_stock_qty, + "incoming_rate": previous_stock_qty < 0 and \ + flt(row.valuation_rate) or 0}, row) + if change_in_qty: if row.valuation_rate == "": @@ -213,13 +224,17 @@ class DocType(DocListController): "voucher_type": self.doc.doctype, "voucher_no": self.doc.name, "company": webnotes.conn.get_default("company"), - "is_cancelled": "No" + "is_cancelled": "No", } args.update(opts) + # create stock ledger entry sle_wrapper = webnotes.model_wrapper([args]).insert() + + # update bin + webnotes.get_obj('Warehouse', row.warehouse).update_bin(args) - update_entries_after(args) + # update_entries_after(args) return sle_wrapper diff --git a/stock/doctype/stock_reconciliation/stock_reconciliation.txt b/stock/doctype/stock_reconciliation/stock_reconciliation.txt index 272bf99f17..ddd7e0889e 100644 --- a/stock/doctype/stock_reconciliation/stock_reconciliation.txt +++ b/stock/doctype/stock_reconciliation/stock_reconciliation.txt @@ -2,9 +2,9 @@ { "owner": "Administrator", "docstatus": 0, - "creation": "2013-01-09 11:24:35", + "creation": "2013-01-11 12:04:17", "modified_by": "Administrator", - "modified": "2013-01-10 19:26:28" + "modified": "2013-01-11 15:36:21" }, { "allow_attach": 0, @@ -29,11 +29,18 @@ "parentfield": "fields" }, { - "name": "__common__", "parent": "Stock Reconciliation", "read": 1, "doctype": "DocPerm", + "cancel": 1, + "name": "__common__", + "amend": 1, + "create": 1, + "submit": 1, + "write": 1, "parenttype": "DocType", + "role": "Material Manager", + "permlevel": 0, "parentfield": "permissions" }, { @@ -77,22 +84,6 @@ "fieldname": "col1", "fieldtype": "Column Break" }, - { - "read_only": 0, - "oldfieldtype": "Text", - "doctype": "DocField", - "label": "Remark", - "oldfieldname": "remark", - "fieldname": "remark", - "fieldtype": "Text" - }, - { - "depends_on": "eval:doc.docstatus===0", - "doctype": "DocField", - "label": "Upload", - "fieldname": "sb1", - "fieldtype": "Section Break" - }, { "read_only": 1, "print_hide": 1, @@ -102,6 +93,7 @@ "fieldtype": "HTML" }, { + "depends_on": "reconciliation_json", "doctype": "DocField", "label": "Reconciliation Data", "fieldname": "sb2", @@ -127,32 +119,6 @@ "hidden": 1 }, { - "amend": 0, - "create": 1, - "doctype": "DocPerm", - "submit": 1, - "write": 1, - "cancel": 1, - "role": "Material Manager", - "permlevel": 0 - }, - { - "amend": 0, - "create": 0, - "doctype": "DocPerm", - "submit": 0, - "write": 0, - "cancel": 0, - "role": "Material Manager", - "permlevel": 1 - }, - { - "create": 1, - "doctype": "DocPerm", - "submit": 1, - "write": 1, - "cancel": 1, - "role": "System Manager", - "permlevel": 0 + "doctype": "DocPerm" } ] \ No newline at end of file diff --git a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index fadc3b4139..224d70e05d 100644 --- a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -36,18 +36,19 @@ class TestStockReconciliation(unittest.TestCase): webnotes.conn.rollback() def test_reco_for_fifo(self): - # [[qty, valuation_rate, posting_date, posting_time]] + # [[qty, valuation_rate, posting_date, posting_time, expected_stock_value, bin_qty]] input_data = [ - [50, 1000, "2012-12-26", "12:00", 50000], - [5, 1000, "2012-12-26", "12:00", 5000], - [15, 1000, "2012-12-26", "12:00", 15000], - [25, 900, "2012-12-26", "12:00", 22500], - [20, 500, "2012-12-26", "12:00", 10000], - [50, 1000, "2013-01-01", "12:00", 50000], - [5, 1000, "2013-01-01", "12:00", 5000], - ["", 1000, "2012-12-26", "12:05", 15000], - [20, "", "2012-12-26", "12:05", 16000], - [10, 2000, "2012-12-26", "12:10", 20000] + [50, 1000, "2012-12-26", "12:00", 50000, 45, 48000], + [5, 1000, "2012-12-26", "12:00", 5000, 0, 0], + [15, 1000, "2012-12-26", "12:00", 15000, 10, 12000], + [25, 900, "2012-12-26", "12:00", 22500, 20, 22500], + [20, 500, "2012-12-26", "12:00", 10000, 15, 18000], + [50, 1000, "2013-01-01", "12:00", 50000, 65, 68000], + [5, 1000, "2013-01-01", "12:00", 5000, 20, 23000], + ["", 1000, "2012-12-26", "12:05", 15000, 10, 12000], + [20, "", "2012-12-26", "12:05", 16000, 15, 18000], + [10, 2000, "2012-12-26", "12:10", 20000, 5, 6000], + [1, 1000, "2012-12-01", "00:00", 1000, 11, 13200], ] for d in input_data: @@ -60,14 +61,19 @@ class TestStockReconciliation(unittest.TestCase): and posting_date = %s and posting_time = %s order by name desc limit 1""", (d[2], d[3])) - # stock_value = sum([v[0]*v[1] for v in json.loads(res and res[0][0] or "[]")]) self.assertEqual(res and flt(res[0][0]) or 0, d[4]) + bin = webnotes.conn.sql("""select actual_qty, stock_value from `tabBin` + where item_code = 'Android Jack D' and warehouse = 'Default Warehouse'""") + + self.assertEqual(bin and [flt(bin[0][0]), flt(bin[0][1])] or [], [d[5], d[6]]) + + self.tearDown() self.setUp() - def test_reco_for_moving_average(self): + def atest_reco_for_moving_average(self): # [[qty, valuation_rate, posting_date, posting_time]] input_data = [ [50, 1000, "2012-12-26", "12:00", 50000], @@ -79,7 +85,8 @@ class TestStockReconciliation(unittest.TestCase): [5, 1000, "2013-01-01", "12:00", 5000], ["", 1000, "2012-12-26", "12:05", 15000], [20, "", "2012-12-26", "12:05", 18000], - [10, 2000, "2012-12-26", "12:10", 20000] + [10, 2000, "2012-12-26", "12:10", 20000], + [1, 1000, "2012-12-01", "00:00", 1000], ] for d in input_data: @@ -96,7 +103,7 @@ class TestStockReconciliation(unittest.TestCase): self.tearDown() self.setUp() - + def submit_stock_reconciliation(self, qty, rate, posting_date, posting_time): return webnotes.model_wrapper([{ "doctype": "Stock Reconciliation", @@ -168,12 +175,4 @@ class TestStockReconciliation(unittest.TestCase): }, ] - # pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' - # and warehouse='Default Warehouse'""", as_dict=1)) - - webnotes.get_obj("Stock Ledger").update_stock(existing_ledgers) - - # pprint(webnotes.conn.sql("""select * from `tabBin` where item_code='Android Jack D' - # and warehouse='Default Warehouse'""", as_dict=1)) - - \ No newline at end of file + webnotes.get_obj("Stock Ledger").update_stock(existing_ledgers) \ No newline at end of file diff --git a/stock/doctype/warehouse/warehouse.py b/stock/doctype/warehouse/warehouse.py index 775f0d0302..8d6065c5f8 100644 --- a/stock/doctype/warehouse/warehouse.py +++ b/stock/doctype/warehouse/warehouse.py @@ -35,15 +35,13 @@ class DocType: warehouse = %s", (item_code, warehouse)) bin = bin and bin[0][0] or '' if not bin: - bin = Document('Bin') - bin.item_code = item_code - bin.stock_uom = webnotes.conn.get_value('Item', item_code, 'stock_uom') - bin.warehouse = warehouse - bin.warehouse_type = webnotes.conn.get_value("Warehouse", warehouse, "warehouse_type") - bin_obj = get_obj(doc=bin) - bin_obj.validate() - bin.save(1) - bin = bin.name + bin_wrapper = webnotes.model_wrapper([{ + "doctype": "Bin", + "item_code": item_code, + "warehouse": warehouse, + }]).insert() + + bin_obj = bin_wrapper.make_obj() else: bin_obj = get_obj('Bin', bin) return bin_obj diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index f88ea5d0f6..3cad35559c 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -89,6 +89,14 @@ def update_entries_after(args, verbose=1): _raise_exceptions(args, verbose) # update bin + if not webnotes.conn.exists({"doctype": "Bin", "item_code": args["item_code"], + "warehouse": args["warehouse"]}): + webnotes.model_wrapper([{ + "doctype": "Bin", + "item_code": args["item_code"], + "warehouse": args["warehouse"], + }]).insert() + 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) @@ -209,7 +217,7 @@ def get_moving_average_values(qty_after_transaction, sle, valuation_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]) From 2ab31ab2d3aed3dd686b579620418fea4b6a5d93 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 11:17:34 +0530 Subject: [PATCH 10/18] updated stock reconciliation patch --- .../january_2013/file_list_rename_returns.py | 66 +++++++++++++++++++ .../stock_reconciliation_patch.py | 20 ++++-- patches/patch_list.py | 4 ++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 patches/january_2013/file_list_rename_returns.py diff --git a/patches/january_2013/file_list_rename_returns.py b/patches/january_2013/file_list_rename_returns.py new file mode 100644 index 0000000000..cca7a15725 --- /dev/null +++ b/patches/january_2013/file_list_rename_returns.py @@ -0,0 +1,66 @@ +import webnotes +from webnotes.utils import get_base_path +import os + +def execute(): + # find out when was the file list patch run + res = webnotes.conn.sql("""select applied_on from `__PatchLog` + where patch='patches.december_2012.file_list_rename' order by applied_on desc limit 1""") + if res: + patch_date = res[0][0].date() + files_path = os.path.join(get_base_path(), "public", "files") + + change_map = {} + + file_data_list = webnotes.conn.sql("""select name, file_name from `tabFile Data` + where date(modified) <= %s and ifnull(file_url, '')='' and name like "%%-%%" """, + patch_date) + + # print patch_date + # print file_data_list + # print files_path + + for fid, file_name in file_data_list: + if os.path.exists(os.path.join(files_path, fid)): + new_fid, new_file_name = fid.replace("-", ""), file_name.replace("-", "") + + try: + webnotes.conn.sql("""update `tabFile Data` + set name=%s, file_name=%s where name=%s""", (new_fid, new_file_name, fid)) + + os.rename(os.path.join(files_path, fid), os.path.join(files_path, new_fid)) + + change_map[",".join((file_name, fid))] = ",".join((new_file_name, new_fid)) + except Exception, e: + # if duplicate entry, then dont update + if e[0]!=1062: + print webnotes.getTraceback() + raise e + + print change_map + + changed_keys = change_map.keys() + + for dt in webnotes.conn.sql("""select distinct parent from tabDocField + where fieldname='file_list'"""): + try: + data = webnotes.conn.sql("""select name, file_list from `tab%s` + where ifnull(file_list, '')!=''""" % dt[0]) + for name, file_list in data: + new_file_list = [] + file_list = file_list.split("\n") + for f in file_list: + if f in changed_keys: + new_file_list.append(change_map[f]) + else: + new_file_list.append(f) + if new_file_list != file_list: + webnotes.conn.sql("""update `tab%s` set file_list=%s + where name=%s""" % (dt[0], "%s", "%s"), + ("\n".join(new_file_list), name)) + + except Exception, e: + if e[0]!=1146: + print webnotes.getTraceback() + raise e + \ No newline at end of file diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 75dab765e2..9d72f58c09 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -15,19 +15,29 @@ def rename_fields(): def store_stock_reco_json(): import os - import conf import json from webnotes.utils.datautils import read_csv_content - base_path = os.path.dirname(os.path.abspath(conf.__file__)) + from webnotes.utils import get_base_path + files_path = os.path.join(get_base_path(), "public", "files") + + list_of_files = os.listdir(files_path) + replaced_list_of_files = [f.replace("-", "") for f in list_of_files] for reco, file_list in webnotes.conn.sql("""select name, file_list from `tabStock Reconciliation`"""): if file_list: file_list = file_list.split("\n") stock_reco_file = file_list[0].split(",")[1] - stock_reco_file = os.path.join(base_path, "public", "files", stock_reco_file) - if os.path.exists(stock_reco_file): - with open(stock_reco_file, "r") as open_reco_file: + stock_reco_file_path = os.path.join(files_path, stock_reco_file) + if not os.path.exists(stock_reco_file_path): + if stock_reco_file in replaced_list_of_files: + stock_reco_file_path = os.path.join(files_path, + list_of_files[replaced_list_of_files.index(stock_reco_file)]) + else: + stock_reco_file_path = "" + + if stock_reco_file_path: + with open(stock_reco_file_path, "r") as open_reco_file: content = open_reco_file.read() content = read_csv_content(content) webnotes.conn.set_value("Stock Reconciliation", reco, "reconciliation_json", diff --git a/patches/patch_list.py b/patches/patch_list.py index f9bb97cf8a..e842e163b8 100644 --- a/patches/patch_list.py +++ b/patches/patch_list.py @@ -582,4 +582,8 @@ patch_list = [ 'patch_module': 'patches.january_2013', 'patch_file': 'holiday_list_patch', }, + { + 'patch_module': 'patches.january_2013', + 'patch_file': 'stock_reconciliation_patch', + }, ] \ No newline at end of file From 914c6df4782653152102994475f4af64697623da Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 14 Jan 2013 13:15:42 +0530 Subject: [PATCH 11/18] testcase for stock reco --- .../test_stock_reconciliation.py | 36 +++++++++++-------- stock/stock_ledger.py | 22 ++++++------ 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 224d70e05d..fb85f653a0 100644 --- a/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -36,7 +36,8 @@ class TestStockReconciliation(unittest.TestCase): webnotes.conn.rollback() def test_reco_for_fifo(self): - # [[qty, valuation_rate, posting_date, posting_time, expected_stock_value, bin_qty]] + # [[qty, valuation_rate, posting_date, + # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ [50, 1000, "2012-12-26", "12:00", 50000, 45, 48000], [5, 1000, "2012-12-26", "12:00", 5000, 0, 0], @@ -73,20 +74,21 @@ class TestStockReconciliation(unittest.TestCase): self.setUp() - def atest_reco_for_moving_average(self): - # [[qty, valuation_rate, posting_date, posting_time]] + def test_reco_for_moving_average(self): + # [[qty, valuation_rate, posting_date, + # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ - [50, 1000, "2012-12-26", "12:00", 50000], - [5, 1000, "2012-12-26", "12:00", 5000], - [15, 1000, "2012-12-26", "12:00", 15000], - [25, 900, "2012-12-26", "12:00", 22500], - [20, 500, "2012-12-26", "12:00", 10000], - [50, 1000, "2013-01-01", "12:00", 50000], - [5, 1000, "2013-01-01", "12:00", 5000], - ["", 1000, "2012-12-26", "12:05", 15000], - [20, "", "2012-12-26", "12:05", 18000], - [10, 2000, "2012-12-26", "12:10", 20000], - [1, 1000, "2012-12-01", "00:00", 1000], + [50, 1000, "2012-12-26", "12:00", 50000, 45, 48000], + [5, 1000, "2012-12-26", "12:00", 5000, 0, 0], + [15, 1000, "2012-12-26", "12:00", 15000, 10, 12000], + [25, 900, "2012-12-26", "12:00", 22500, 20, 22500], + [20, 500, "2012-12-26", "12:00", 10000, 15, 18000], + [50, 1000, "2013-01-01", "12:00", 50000, 65, 68000], + [5, 1000, "2013-01-01", "12:00", 5000, 20, 23000], + ["", 1000, "2012-12-26", "12:05", 15000, 10, 12000], + [20, "", "2012-12-26", "12:05", 18000, 15, 18000], + [10, 2000, "2012-12-26", "12:10", 20000, 5, 6000], + [1, 1000, "2012-12-01", "00:00", 1000, 11, 13200], ] for d in input_data: @@ -101,6 +103,12 @@ class TestStockReconciliation(unittest.TestCase): self.assertEqual(res and flt(res[0][0], 4) or 0, d[4]) + bin = webnotes.conn.sql("""select actual_qty, stock_value from `tabBin` + where item_code = 'Android Jack D' and warehouse = 'Default Warehouse'""") + + self.assertEqual(bin and [flt(bin[0][0]), flt(bin[0][1], 4)] or [], + [flt(d[5]), flt(d[6])]) + self.tearDown() self.setUp() diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index 3cad35559c..7d19c9c814 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -56,13 +56,13 @@ def update_entries_after(args, verbose=1): continue if sle.serial_no: - valuation_rate, incoming_rate = get_serialized_values(qty_after_transaction, sle, + valuation_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 = get_moving_average_values(qty_after_transaction, sle, valuation_rate) else: - valuation_rate, incoming_rate = get_fifo_values(qty_after_transaction, sle, + valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue) qty_after_transaction += flt(sle.actual_qty) @@ -75,15 +75,13 @@ def update_entries_after(args, verbose=1): (qty_after_transaction * valuation_rate) or 0 else: stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue)) - - # print sle.posting_date, qty_after_transaction, incoming_rate, valuation_rate - + # 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""", + stock_value=%s where name=%s""", (qty_after_transaction, valuation_rate, - json.dumps(stock_queue), stock_value, incoming_rate, sle.name)) + json.dumps(stock_queue), stock_value, sle.name)) if _exceptions: _raise_exceptions(args, verbose) @@ -188,11 +186,11 @@ def get_serialized_values(qty_after_transaction, sle, valuation_rate): # else it remains the same as that of previous entry valuation_rate = new_stock_value / new_stock_qty - return valuation_rate, incoming_rate + return valuation_rate def get_moving_average_values(qty_after_transaction, sle, valuation_rate): incoming_rate = flt(sle.incoming_rate) - actual_qty = flt(sle.actual_qty) + actual_qty = flt(sle.actual_qty) if not incoming_rate: # In case of delivery/stock issue in_rate = 0 or wrong incoming rate @@ -212,7 +210,7 @@ def get_moving_average_values(qty_after_transaction, sle, valuation_rate): # NOTE: val_rate is same as previous entry if new stock value is negative - return valuation_rate, incoming_rate + return valuation_rate def get_fifo_values(qty_after_transaction, sle, stock_queue): incoming_rate = flt(sle.incoming_rate) @@ -255,7 +253,7 @@ def get_fifo_values(qty_after_transaction, sle, stock_queue): valuation_rate = stock_qty and (stock_value / flt(stock_qty)) or 0 - return valuation_rate, incoming_rate + return valuation_rate def _raise_exceptions(args, verbose=1): deficiency = min(e["diff"] for e in _exceptions) From 4c3217b11cef2a9516e020ddb6596dc469eefafa Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 14:54:12 +0530 Subject: [PATCH 12/18] updated latest updates page --- home/page/latest_updates/latest_updates.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/home/page/latest_updates/latest_updates.js b/home/page/latest_updates/latest_updates.js index f5561abe86..bcd78d5f21 100644 --- a/home/page/latest_updates/latest_updates.js +++ b/home/page/latest_updates/latest_updates.js @@ -1,4 +1,7 @@ erpnext.updates = [ + ["14th January, 2013", [ + "Stock Reconciliation: Ability to update Valuation Rate" + ]], ["10th January 2013", [ "Modules: New module pages with open item count and multi-lingual.", "Permissions: Added new 'Report' permission. Only users with report permissions will be allowed.", @@ -162,8 +165,7 @@ erpnext.updates = [ "Query Report: Allow user to rename and save reports.", "Employee Leave Balance Report: Bugfix" ]] -] - +]; wn.pages['latest-updates'].onload = function(wrapper) { wn.ui.make_app_page({ @@ -182,7 +184,7 @@ wn.pages['latest-updates'].onload = function(wrapper) { $("

" + day[0] + "

").appendTo(parent); $.each(day[1], function(j, item) { $("

").html(item).appendTo(parent); - }) + }); $("


").appendTo(parent); }); -} \ No newline at end of file +}; \ No newline at end of file From 34ad33bfcb67dcb972670ea8453b734851528e44 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 14:57:16 +0530 Subject: [PATCH 13/18] fix in reconciliation patch --- patches/january_2013/stock_reconciliation_patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 12035954f7..91622e7b44 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -58,5 +58,5 @@ def store_stock_reco_json(): content = read_csv_content(content) reconciliation_json = json.dumps(content, separators=(',', ': ')) webnotes.conn.sql("""update `tabStock Reconciliation` - set reconciliation_json=%s where name=%s""", (reconciliation_json, name)) + set reconciliation_json=%s where name=%s""", (reconciliation_json, reco)) \ No newline at end of file From 1649c60b0bc3f3c2e743381019d0ef4e4cb39071 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 14:58:55 +0530 Subject: [PATCH 14/18] fix in reconciliation patch --- patches/january_2013/stock_reconciliation_patch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 91622e7b44..1cde07c35e 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -2,6 +2,7 @@ import webnotes def execute(): webnotes.reload_doc("stock", "doctype", "stock_ledger_entry") + webnotes.reload_doc("stock", "doctype", "stock_reconciliation") rename_fields() move_remarks_to_comments() From 54d6f1840232ae1d08f41485b2be1a62fb5cf0f4 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 15:06:53 +0530 Subject: [PATCH 15/18] if local, then setup defaults --- public/js/utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/js/utils.js b/public/js/utils.js index fe79248c59..805f578bdf 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -24,7 +24,9 @@ erpnext.utils.Controller = Class.extend({ }, onload_post_render: function() { - this.setup_defaults(); + if(this.frm.doc.__islocal) { + this.setup_defaults(); + } }, setup_defaults: function() { From b1cf2119e9fd2b3126c04b40d5801afcd2bcd202 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 15:12:44 +0530 Subject: [PATCH 16/18] fixes in reconciliation patch --- patches/january_2013/stock_reconciliation_patch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 1cde07c35e..26d8f4f8bf 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -10,7 +10,9 @@ def execute(): def rename_fields(): args = [["Stock Ledger Entry", "bin_aqat", "qty_after_transaction"], - ["Stock Ledger Entry", "fcfs_stack", "stock_queue"]] + ["Stock Ledger Entry", "fcfs_stack", "stock_queue"], + ["Stock Reconciliation", "reconciliation_date", "posting_date"], + ["Stock Reconciliation", "reconciliation_time", "posting_time"]] for doctype, old_fieldname, new_fieldname in args: webnotes.conn.sql("""update `tab%s` set `%s`=`%s`""" % (doctype, new_fieldname, old_fieldname)) From 39a3c03b6233686fe7831d6bc981d233d2f95258 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 15:26:51 +0530 Subject: [PATCH 17/18] if not a valid csv file, ignore it in stock reconciliation patch --- patches/january_2013/stock_reconciliation_patch.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/patches/january_2013/stock_reconciliation_patch.py b/patches/january_2013/stock_reconciliation_patch.py index 26d8f4f8bf..fa919f4598 100644 --- a/patches/january_2013/stock_reconciliation_patch.py +++ b/patches/january_2013/stock_reconciliation_patch.py @@ -58,8 +58,13 @@ def store_stock_reco_json(): if stock_reco_file_path: with open(stock_reco_file_path, "r") as open_reco_file: content = open_reco_file.read() - content = read_csv_content(content) - reconciliation_json = json.dumps(content, separators=(',', ': ')) - webnotes.conn.sql("""update `tabStock Reconciliation` - set reconciliation_json=%s where name=%s""", (reconciliation_json, reco)) + try: + content = read_csv_content(content) + reconciliation_json = json.dumps(content, separators=(',', ': ')) + webnotes.conn.sql("""update `tabStock Reconciliation` + set reconciliation_json=%s where name=%s""", + (reconciliation_json, reco)) + except Exception: + # if not a valid CSV file, do nothing + pass \ No newline at end of file From c313d662dffe38f6bab6707e3f95af3657cda7ab Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 14 Jan 2013 15:46:17 +0530 Subject: [PATCH 18/18] -m --- stock/doctype/stock_ledger/stock_ledger.py | 4 +++- stock/stock_ledger.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/stock/doctype/stock_ledger/stock_ledger.py b/stock/doctype/stock_ledger/stock_ledger.py index 10a905fad5..86a8663635 100644 --- a/stock/doctype/stock_ledger/stock_ledger.py +++ b/stock/doctype/stock_ledger/stock_ledger.py @@ -219,7 +219,9 @@ class DocType: if args.get("warehouse"): args["warehouse_type"] = webnotes.conn.get_value('Warehouse' , args["warehouse"], 'warehouse_type') - sle = webnotes.model_wrapper([args]).insert() + sle = webnotes.model_wrapper([args]) + sle.ignore_permissions = 1 + sle.insert() return sle.doc.name def repost(self): diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py index 7d19c9c814..db88f6b12b 100644 --- a/stock/stock_ledger.py +++ b/stock/stock_ledger.py @@ -89,11 +89,13 @@ def update_entries_after(args, verbose=1): # update bin if not webnotes.conn.exists({"doctype": "Bin", "item_code": args["item_code"], "warehouse": args["warehouse"]}): - webnotes.model_wrapper([{ + bin_wrapper = webnotes.model_wrapper([{ "doctype": "Bin", "item_code": args["item_code"], "warehouse": args["warehouse"], - }]).insert() + }]) + bin_wrapper.ignore_permissions = 1 + bin_wrapper.insert() webnotes.conn.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value=%s,