From 9d0f636c46fa22da8103a07172bffa25955cf0be Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 7 Jan 2013 18:51:11 +0530 Subject: [PATCH] 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