This commit is contained in:
Rushabh Mehta 2013-01-14 16:11:03 +05:30
commit 3c5161113e
37 changed files with 1876 additions and 1090 deletions

View File

@ -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;
})

View File

@ -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) {
$("<h4>" + day[0] + "</h4>").appendTo(parent);
$.each(day[1], function(j, item) {
$("<p>").html(item).appendTo(parent);
})
});
$("<hr>").appendTo(parent);
});
}
};

View File

@ -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)

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import webnotes
from stock.stock_ledger import update_entries_after
def execute():
# add index
@ -80,7 +81,12 @@ 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
@ -92,10 +98,3 @@ def create_comment(dn):
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')

View File

@ -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',

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
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");
}
});

View File

@ -24,7 +24,9 @@ erpnext.utils.Controller = Class.extend({
},
onload_post_render: function() {
if(this.frm.doc.__islocal) {
this.setup_defaults();
}
},
setup_defaults: function() {

View File

@ -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)

View File

@ -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):
self.update_qty(args)
if args.get("actual_qty"):
from stock.stock_ledger import update_entries_after
if not args.get("posting_date"):
posting_date = nowdate()
self.update_qty(args)
if (flt(args.get("actual_qty")) < 0 or flt(args.get("reserved_qty")) > 0) \
and args.get("is_cancelled") == 'No' and args.get("is_amended")=='No':
self.reorder_item(args.get("voucher_type"), args.get("voucher_no"))
if args.get("actual_qty"):
# 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"))
@ -58,6 +80,10 @@ class DocType:
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("""
select * from `tabStock Ledger Entry`
@ -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 <b>%s</b> in Warehouse
<b>%s</b> on <b>%s %s</b> in Transaction %s %s.
Total Quantity Deficiency: <b>%s</b>""" % \
(self.exc_list[0]["diff"], self.doc.item_code, self.doc.warehouse,
self.exc_list[0]["posting_date"], self.exc_list[0]["posting_time"],
self.exc_list[0]["voucher_type"], self.exc_list[0]["voucher_no"],
abs(deficiency))
if verbose:
msgprint(msg, raise_exception=1)
else:
raise webnotes.ValidationError, msg
# update the bin
if sll or not prev_sle:
sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value = %s,
projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty -
reserved_qty) where name=%s
""", (flt(val_rate), cqty, flt(stock_val), self.doc.name))
def reorder_item(self,doc_type,doc_name):
""" 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

View File

@ -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):

View File

@ -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)
# ---------

View File

@ -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 <http://www.gnu.org/licenses/>.
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")

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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);
}

View File

@ -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,43 +157,24 @@ class DocType(TransactionBase):
def get_stock_and_rate(self):
"""get stock and incoming rate on posting date"""
for d in getlist(self.doclist, 'mtn_details'):
args = {
"item_code": d.item_code,
"warehouse": d.s_warehouse or d.t_warehouse,
"posting_date": self.doc.posting_date,
"posting_time": self.doc.posting_time,
"qty": d.transfer_qty,
"serial_no": d.serial_no,
"bom_no": d.bom_no
}
# get actual stock at source warehouse
d.actual_qty = self.get_as_on_stock(d.item_code, d.s_warehouse or d.t_warehouse,
self.doc.posting_date, self.doc.posting_time)
d.actual_qty = get_previous_sle(args).get("qty_after_transaction") or 0
# get incoming rate
if not flt(d.incoming_rate):
d.incoming_rate = self.get_incoming_rate(d.item_code,
d.s_warehouse or d.t_warehouse, self.doc.posting_date,
self.doc.posting_time, d.transfer_qty, d.serial_no, d.bom_no)
d.incoming_rate = get_incoming_rate(args)
d.amount = flt(d.qty) * flt(d.incoming_rate)
def get_as_on_stock(self, item_code, warehouse, posting_date, posting_time):
"""Get stock qty on any date"""
bin = sql("select name from tabBin where item_code = %s and warehouse = %s",
(item_code, warehouse))
if bin:
prev_sle = get_obj('Bin', bin[0][0]).get_prev_sle(posting_date, posting_time)
return flt(prev_sle.get("bin_aqat")) or 0
else:
return 0
def get_incoming_rate(self, item_code=None, warehouse=None,
posting_date=None, posting_time=None, qty=0, serial_no=None, bom_no=None):
in_rate = 0
if bom_no:
result = webnotes.conn.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1)
from `tabBOM` where name = %s and docstatus=1 and is_active=1""",
(bom_no,))
in_rate = result and flt(result[0][0]) or 0
elif warehouse:
in_rate = get_obj("Valuation Control").get_incoming_rate(posting_date, posting_time,
item_code, warehouse, qty, serial_no)
return in_rate
def validate_incoming_rate(self):
for d in getlist(self.doclist, 'mtn_details'):
if not flt(d.incoming_rate) and d.t_warehouse:
@ -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

View File

@ -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
},
{

View File

@ -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",

View File

@ -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))
@ -94,10 +80,6 @@ class DocType:
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
from `tabItem` where name = '%s' and (ifnull(end_of_life,'')='' or
@ -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,18 +215,14 @@ 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):
"""

View File

@ -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'

View File

@ -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"
}
]

View File

@ -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 <http://www.gnu.org/licenses/>.
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(": <b>(.*)</b>")
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()

View File

@ -14,10 +14,112 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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 = $('<div id="dit-upload-area"></div>').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 "<th>" + col + "</th>"; }
: function(col) { return "<td>" + col + "</td>"; };
$.each(data, function(i, row) {
result += "<tr>"
+ $.map(row, _render).join("")
+ "</tr>";
});
return result;
};
var $reconciliation_table = $("<div style='overflow-x: auto;'>\
<table class='table table-striped table-bordered'>\
<thead>" + _make([reconciliation_data[0]], true) + "</thead>\
<tbody>" + _make(reconciliation_data.splice(1)) + "</tbody>\
</table>\
</div>").appendTo($wrapper);
}
},
});
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});

View File

@ -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
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
import json
from webnotes import msgprint, _
from webnotes.utils import cstr, flt
from webnotes.model.controller import DocListController
from stock.stock_ledger import update_entries_after
class DocType(DocListController):
def setup(self):
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
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)
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)
self.validate_data()
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 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)
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)
# remove the help part and save the json
if data.index(self.head_row) != 0:
data = data[data.index(self.head_row):]
self.doc.reconciliation_json = json.dumps(data)
def _get_msg(row_num, msg):
return _("Row # ") + ("%d: " % (row_num+2)) + _(msg)
self.validation_messages = []
item_warehouse_combinations = []
for row_num, row in enumerate(data[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]])
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
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])
item = webnotes.doc("Item", item_code)
if errors:
import re
error_msg = [["Item Code", "Warehouse", "Qty"]]
qty_regex = re.compile(": <b>(.*)</b>")
for e in errors:
qty = qty_regex.findall(unicode(e[2]))
qty = qty and abs(flt(qty[0])) or None
# 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)
error_msg.append([e[0], e[1], flt(qty)])
# 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."""))
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)
# 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()

View File

@ -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": "<div class=\"field_description\"><b>Steps:</b><br>1. Enter Reconciliation Date and Time<br>2. Save the document<br>3. Attach csv file as per template.<br>4. Submit the document<br>5. Enter tilde (~) sign if no difference in qty or valuation rate</div>",
"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"
}
]

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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.")

View File

@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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"
}
]

View File

@ -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("""

View File

@ -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"
},
{

295
stock/stock_ledger.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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 <b>%s</b> in Warehouse
<b>%s</b> on <b>%s %s</b> in Transaction %s %s.
Total Quantity Deficiency: <b>%s</b>""" % \
(_exceptions[0]["diff"], args.get("item_code"), args.get("warehouse"),
_exceptions[0]["posting_date"], _exceptions[0]["posting_time"],
_exceptions[0]["voucher_type"], _exceptions[0]["voucher_no"],
abs(deficiency))
if verbose:
msgprint(msg, raise_exception=1)
else:
raise webnotes.ValidationError, msg
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 {}

134
stock/utils.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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"
}
]