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;
})
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
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/april_2012/repost_stock_for_posting_time.py b/patches/april_2012/repost_stock_for_posting_time.py
index a1283a0327..d9cbbe5eb0 100644
--- a/patches/april_2012/repost_stock_for_posting_time.py
+++ b/patches/april_2012/repost_stock_for_posting_time.py
@@ -1,10 +1,13 @@
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,
+ })
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
new file mode 100644
index 0000000000..fa919f4598
--- /dev/null
+++ b/patches/january_2013/stock_reconciliation_patch.py
@@ -0,0 +1,70 @@
+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()
+ store_stock_reco_json()
+
+def rename_fields():
+ args = [["Stock Ledger Entry", "bin_aqat", "qty_after_transaction"],
+ ["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))
+
+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 json
+ from webnotes.utils.datautils import read_csv_content
+ 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_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()
+ 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
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..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
@@ -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/patch_list.py b/patches/patch_list.py
index 4d3c780642..0b6c3f82db 100644
--- a/patches/patch_list.py
+++ b/patches/patch_list.py
@@ -582,6 +582,10 @@ patch_list = [
'patch_module': 'patches.january_2013',
'patch_file': 'holiday_list_patch',
},
+ {
+ 'patch_module': 'patches.january_2013',
+ 'patch_file': 'stock_reconciliation_patch',
+ },
{
'patch_module': 'patches.january_2013',
'patch_file': 'report_permission',
diff --git a/patches/september_2012/repost_stock.py b/patches/september_2012/repost_stock.py
index c6b6ce39f7..972070137a 100644
--- a/patches/september_2012/repost_stock.py
+++ b/patches/september_2012/repost_stock.py
@@ -17,12 +17,12 @@
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] })
except:
pass
i += 1
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/public/js/utils.js b/public/js/utils.js
index 271dab14d4..436c532d82 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() {
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..19ce8f9e51 100644
--- a/stock/doctype/bin/bin.py
+++ b/stock/doctype/bin/bin.py
@@ -31,24 +31,46 @@ 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):
- 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
- 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))
- 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"))
@@ -57,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("""
@@ -69,253 +95,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
- # are reposted from the current time-bucket onwards.
- # this is necessary because at the time of cancellation, there may be
- # entries between the cancelled entries in the same time-bucket
-
- sle = sql("""
- select * from `tabStock Ledger Entry`
- where item_code = %s
- and warehouse = %s
- and ifnull(is_cancelled, 'No') = 'No'
- and timestamp(posting_date, posting_time) < timestamp(%s, %s)
- order by timestamp(posting_date, posting_time) desc, name desc
- limit 1
- """, (self.doc.item_code, self.doc.warehouse, posting_date, posting_time), as_dict=1)
-
- return sle and sle[0] or {}
-
- def validate_negative_stock(self, cqty, s):
- """
- validate negative stock for entries current datetime onwards
- will not consider cancelled entries
- """
- diff = cqty + s['actual_qty']
- if diff < 0 and (abs(diff) > 0.0001) and s['is_cancelled'] == 'No':
- self.exc_list.append({
- "diff": diff,
- "posting_date": s["posting_date"],
- "posting_time": s["posting_time"],
- "voucher_type": s["voucher_type"],
- "voucher_no": s["voucher_no"]
- })
- return True
- else:
- return False
-
- def get_serialized_inventory_values(self, val_rate, in_rate, opening_qty, \
- actual_qty, is_cancelled, serial_nos):
- """
- get serialized inventory values
- """
- if flt(in_rate) < 0: # wrong incoming rate
- in_rate = val_rate
- elif flt(in_rate) == 0 or flt(actual_qty) < 0:
- # In case of delivery/stock issue, get average purchase rate
- # of serial nos of current entry
- in_rate = flt(sql("""select ifnull(avg(purchase_rate), 0)
- from `tabSerial No` where name in (%s)""" % (serial_nos))[0][0])
-
- if in_rate and val_rate == 0: # First entry
- val_rate = in_rate
- # val_rate is same as previous entry if val_rate is negative
- # Otherwise it will be calculated as per moving average
- elif opening_qty + actual_qty > 0 and ((opening_qty * val_rate) + \
- (actual_qty * in_rate)) > 0:
- val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
- (opening_qty + actual_qty)
- return val_rate, in_rate
-
- def get_moving_average_inventory_values(self, val_rate, in_rate, opening_qty, actual_qty, is_cancelled):
- if flt(in_rate) == 0 or flt(actual_qty) < 0:
- # In case of delivery/stock issue in_rate = 0 or wrong incoming rate
- in_rate = val_rate
-
- # val_rate is same as previous entry if :
- # 1. actual qty is negative(delivery note / stock entry)
- # 2. cancelled entry
- # 3. val_rate is negative
- # Otherwise it will be calculated as per moving average
- if actual_qty > 0 and (opening_qty + actual_qty) > 0 and is_cancelled == 'No' \
- and ((opening_qty * val_rate) + (actual_qty * in_rate)) > 0:
- opening_qty = opening_qty > 0 and opening_qty or 0
- val_rate = ((opening_qty *val_rate) + (actual_qty * in_rate)) / \
- (opening_qty + actual_qty)
- elif (opening_qty + actual_qty) <= 0:
- val_rate = 0
- return val_rate, in_rate
-
- def get_fifo_inventory_values(self, in_rate, actual_qty):
- # add batch to fcfs balance
- if actual_qty > 0:
- self.fcfs_bal.append([flt(actual_qty), flt(in_rate)])
-
- # remove from fcfs balance
- else:
- incoming_cost = 0
- withdraw = flt(abs(actual_qty))
- while withdraw:
- if not self.fcfs_bal:
- break # nothing in store
-
- batch = self.fcfs_bal[0]
-
- if batch[0] <= withdraw:
- # not enough or exactly same qty in current batch, clear batch
- incoming_cost += flt(batch[1])*flt(batch[0])
- withdraw -= batch[0]
- self.fcfs_bal.pop(0)
-
-
- else:
- # 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('bin_aqat', 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 '[]')
-
- # get valuation method
- val_method = get_obj('Valuation Control').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 bin_aqat=%s, valuation_rate=%s, fcfs_stack=%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"""
@@ -382,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/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/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.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.py b/stock/doctype/stock_entry/stock_entry.py
index fcb939a14c..eaf796655d 100644
--- a/stock/doctype/stock_entry/stock_entry.py
+++ b/stock/doctype/stock_entry/stock_entry.py
@@ -23,6 +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_incoming_rate
+from stock.stock_ledger import get_previous_sle
sql = webnotes.conn.sql
@@ -129,8 +131,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 +157,23 @@ 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_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_ledger/stock_ledger.py b/stock/doctype/stock_ledger/stock_ledger.py
index 8d39b26dbb..86a8663635 100644
--- a/stock/doctype/stock_ledger/stock_ledger.py
+++ b/stock/doctype/stock_ledger/stock_ledger.py
@@ -17,29 +17,15 @@
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
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 +46,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 +79,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 +175,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 +184,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 +193,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()
@@ -233,19 +215,15 @@ 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])
+ sle.ignore_permissions = 1
+ sle.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 3e013b6495..2b1af33e50 100644
--- a/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -17,10 +17,11 @@
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
+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(':')) > 3:
- 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..62bc69fcc9 100644
--- a/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -14,10 +14,112 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-cur_frm.cscript.refresh = function(doc) {
- if (doc.docstatus) hide_field('steps');
-}
+wn.require("public/app/js/stock_controller.js");
+wn.provide("erpnext.stock");
-cur_frm.cscript.download_template = function(doc, cdt, cdn) {
- $c_obj_csv(make_doclist(cdt, cdn), 'get_template', '');
-}
+erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
+ refresh: function() {
+ 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 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("");
+ }
+ 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([["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");
+ },
+
+ 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();
+ me.frm.save();
+ }
+ });
+ },
+
+ show_download_reconciliation_data: function() {
+ var me = this;
+ 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 reconciliation_data = JSON.parse(this.frm.doc.reconciliation_json);
+
+ 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..3a8ffcde07 100644
--- a/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -16,243 +16,253 @@
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
+from webnotes.model.controller import DocListController
+from stock.stock_ledger import update_entries_after
-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)
+class DocType(DocListController):
+ def setup(self):
+ self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
- 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.insert_stock_ledger_entries()
+
def on_cancel(self):
- self.cancel_stock_ledger_entries()
- self.update_entries_after()
+ self.delete_stock_ledger_entries()
- 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)
+ def validate_data(self):
+ data = json.loads(self.doc.reconciliation_json)
+ 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)
- 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
+ # 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)
- 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[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"))
+ 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 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_valuation_method
+ from stock.stock_ledger import get_previous_sle
+
+ row_template = ["item_code", "warehouse", "qty", "valuation_rate"]
+
+ data = json.loads(self.doc.reconciliation_json)
+ 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,
+ "warehouse": row.warehouse,
+ "posting_date": self.doc.posting_date,
+ "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, change_in_qty, change_in_rate)
+
+ else:
+ self.sle_for_fifo(row, previous_sle, change_in_qty, change_in_rate)
+
+ 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:
+ 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.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 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.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)
+
+ # -1 entry
+ self.insert_entries({"actual_qty": -1}, row)
+
+ 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.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))
+
+ 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": flt(row.valuation_rate)}, row)
+
+ # Make reverse entry
+ 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 == "":
+ # 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"""
+ args = {
+ "doctype": "Stock Ledger Entry",
+ "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)
+
+ # 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)
+
+ return sle_wrapper
+
+ 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():
+ 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..ddd7e0889e 100644
--- a/stock/doctype/stock_reconciliation/stock_reconciliation.txt
+++ b/stock/doctype/stock_reconciliation/stock_reconciliation.txt
@@ -2,40 +2,45 @@
{
"owner": "Administrator",
"docstatus": 0,
- "creation": "2012-04-13 11:56:39",
+ "creation": "2013-01-11 12:04:17",
"modified_by": "Administrator",
- "modified": "2012-05-10 11:54:52"
+ "modified": "2013-01-11 15:36:21"
},
{
- "section_style": "Tray",
- "allow_attach": 1,
+ "allow_attach": 0,
"is_submittable": 1,
+ "allow_print": 1,
"search_fields": "reconciliation_date",
"module": "Stock",
- "server_code_error": " ",
- "subject": "Date: %(reconciliation_date)s, Time: %(reconciliation_time)s",
- "_last_update": "1321617741",
- "autoname": "SR/.######",
- "name": "__common__",
- "colour": "White:FFF",
"doctype": "DocType",
- "show_in_menu": 0,
+ "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,
- "version": 1
+ "allow_copy": 1
},
{
"name": "__common__",
"parent": "Stock Reconciliation",
"doctype": "DocField",
"parenttype": "DocType",
+ "permlevel": 0,
"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"
},
{
@@ -43,120 +48,77 @@
"doctype": "DocType"
},
{
- "amend": 0,
- "create": 1,
- "doctype": "DocPerm",
- "submit": 1,
- "write": 1,
- "role": "Material Manager",
- "cancel": 1,
- "permlevel": 0
- },
- {
- "amend": 0,
- "create": 0,
- "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
- },
- {
+ "read_only": 0,
"oldfieldtype": "Date",
"doctype": "DocField",
- "label": "Reconciliation Date",
+ "label": "Posting Date",
"oldfieldname": "reconciliation_date",
- "fieldname": "reconciliation_date",
+ "fieldname": "posting_date",
"fieldtype": "Date",
"reqd": 1,
- "permlevel": 0,
"in_filter": 0
},
{
+ "read_only": 0,
"oldfieldtype": "Time",
"doctype": "DocField",
- "label": "Reconciliation Time",
+ "label": "Posting Time",
"oldfieldname": "reconciliation_time",
- "fieldname": "reconciliation_time",
+ "fieldname": "posting_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
- },
- {
+ "read_only": 1,
"print_hide": 1,
"no_copy": 1,
"doctype": "DocField",
"label": "Amended From",
- "permlevel": 1,
"fieldname": "amended_from",
"fieldtype": "Link",
- "options": "Sales Invoice"
+ "options": "Stock Reconciliation"
+ },
+ {
+ "doctype": "DocField",
+ "fieldname": "col1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "read_only": 1,
+ "print_hide": 1,
+ "doctype": "DocField",
+ "label": "Upload HTML",
+ "fieldname": "upload_html",
+ "fieldtype": "HTML"
+ },
+ {
+ "depends_on": "reconciliation_json",
+ "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
+ },
+ {
+ "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
new file mode 100644
index 0000000000..fb85f653a0
--- /dev/null
+++ b/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -0,0 +1,186 @@
+# 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
+from webnotes.utils import flt
+import json
+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:", "\n--\n".join(webnotes.message_log)
+ # print "Debug Log:", "\n--\n".join(webnotes.debug_log)
+ webnotes.conn.rollback()
+
+ def test_reco_for_fifo(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, 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:
+ self.insert_existing_sle("FIFO")
+
+ 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]) 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):
+ # [[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],
+ [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:
+ 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])
+
+ 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()
+
+ def submit_stock_reconciliation(self, qty, rate, posting_date, posting_time):
+ 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 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",
+ "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",
+ "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",
+ "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",
+ "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",
+ "actual_qty": 15, "incoming_rate": 1200, "company": company
+ },
+ ]
+
+ webnotes.get_obj("Stock Ledger").update_stock(existing_ledgers)
\ No newline at end of file
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 7fcf4df499..a02758dfb3 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/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/doctype/warehouse/warehouse.py b/stock/doctype/warehouse/warehouse.py
index 38ba3879d3..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
@@ -104,8 +102,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 +114,12 @@ 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 })
def repost_reserved_qty(self, bin):
reserved_qty = webnotes.conn.sql("""
diff --git a/stock/report/stock_ledger/stock_ledger.txt b/stock/report/stock_ledger/stock_ledger.txt
index 2d8a6404a6..5184560e14 100644
--- a/stock/report/stock_ledger/stock_ledger.txt
+++ b/stock/report/stock_ledger/stock_ledger.txt
@@ -4,13 +4,13 @@
"docstatus": 0,
"creation": "2012-12-03 10:31:11",
"modified_by": "Administrator",
- "modified": "2013-01-14 10:47:15"
+ "modified": "2013-01-14 10:50:15"
},
{
"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\"}",
"is_standard": "Yes"
},
{
diff --git a/stock/stock_ledger.py b/stock/stock_ledger.py
new file mode 100644
index 0000000000..db88f6b12b
--- /dev/null
+++ b/stock/stock_ledger.py
@@ -0,0 +1,295 @@
+# 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, flt, cstr
+from stock.utils import get_valuation_method
+import json
+
+# 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 "[]")
+ stock_value = 0.0
+
+ entries_to_fix = get_sle_after_datetime(previous_sle or \
+ {"item_code": args["item_code"], "warehouse": args["warehouse"]}, for_update=True)
+
+ valuation_method = get_valuation_method(args["item_code"])
+
+ for sle in entries_to_fix:
+ 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_no:
+ valuation_rate = get_serialized_values(qty_after_transaction, sle,
+ valuation_rate)
+ elif valuation_method == "Moving Average":
+ valuation_rate = get_moving_average_values(qty_after_transaction, sle,
+ valuation_rate)
+ else:
+ valuation_rate = get_fifo_values(qty_after_transaction, sle,
+ stock_queue)
+
+ qty_after_transaction += flt(sle.actual_qty)
+
+ # get stock value
+ if sle.serial_no:
+ 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 where name=%s""",
+ (qty_after_transaction, valuation_rate,
+ json.dumps(stock_queue), stock_value, sle.name))
+
+ if _exceptions:
+ _raise_exceptions(args, verbose)
+
+ # update bin
+ if not webnotes.conn.exists({"doctype": "Bin", "item_code": args["item_code"],
+ "warehouse": args["warehouse"]}):
+ bin_wrapper = webnotes.model_wrapper([{
+ "doctype": "Bin",
+ "item_code": args["item_code"],
+ "warehouse": args["warehouse"],
+ }])
+ bin_wrapper.ignore_permissions = 1
+ bin_wrapper.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)
+ 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, for_update=False):
+ """
+ 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)"],
+ "desc", "limit 1", for_update=for_update)
+
+ return sle and sle[0] or webnotes._dict()
+
+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", for_update=for_update)
+
+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"
+ 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) %(order)s, name %(order)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)
+
+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_no = cstr(sle.serial_no).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_no))),
+ tuple(serial_no))[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
+
+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:
+ # In case of delivery/stock issue in_rate = 0 or wrong incoming rate
+ incoming_rate = valuation_rate
+
+ 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 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
+
+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:
+ if not stock_queue:
+ stock_queue.append([0, 0])
+
+ 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
+
+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
+ 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
+
+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
+ 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", for_update=for_update)
+ return sle and sle[0] or {}
\ No newline at end of file
diff --git a/stock/utils.py b/stock/utils.py
new file mode 100644
index 0000000000..a65406beb0
--- /dev/null
+++ b/stock/utils.py
@@ -0,0 +1,134 @@
+# 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, cstr
+
+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_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"):
+ 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
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