2015-03-03 14:55:30 +05:30
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
2013-08-05 14:59:54 +05:30
# License: GNU General Public License v3. See license.txt
2013-11-29 19:15:26 +05:30
from __future__ import unicode_literals
2013-01-08 18:29:24 +05:30
2014-02-14 15:47:51 +05:30
import frappe
2014-04-14 19:20:45 +05:30
from frappe import _
2014-02-14 15:47:51 +05:30
from frappe . utils import cint , flt , cstr , now
2013-12-12 19:12:19 +05:30
from erpnext . stock . utils import get_valuation_method
2013-01-09 15:23:05 +05:30
import json
2013-01-08 18:29:24 +05:30
# future reposting
2014-02-14 15:47:51 +05:30
class NegativeStockError ( frappe . ValidationError ) : pass
2013-01-08 18:29:24 +05:30
2014-02-14 15:47:51 +05:30
_exceptions = frappe . local ( ' stockledger_exceptions ' )
2013-09-18 18:31:03 +05:30
# _exceptions = []
2013-09-25 19:55:41 +05:30
2015-03-27 15:38:31 +05:30
def make_sl_entries ( sl_entries , is_amended = None , allow_negative_stock = False , via_landed_cost_voucher = False ) :
2013-09-26 16:16:44 +05:30
if sl_entries :
2013-12-12 19:12:19 +05:30
from erpnext . stock . utils import update_bin
2014-04-07 12:02:57 +05:30
2013-09-26 16:16:44 +05:30
cancel = True if sl_entries [ 0 ] . get ( " is_cancelled " ) == " Yes " else False
if cancel :
set_as_cancel ( sl_entries [ 0 ] . get ( ' voucher_no ' ) , sl_entries [ 0 ] . get ( ' voucher_type ' ) )
2014-04-07 12:02:57 +05:30
2013-09-26 16:16:44 +05:30
for sle in sl_entries :
sle_id = None
if sle . get ( ' is_cancelled ' ) == ' Yes ' :
sle [ ' actual_qty ' ] = - flt ( sle [ ' actual_qty ' ] )
2014-04-07 12:02:57 +05:30
2014-11-03 15:08:21 +05:30
if sle . get ( " actual_qty " ) or sle . get ( " voucher_type " ) == " Stock Reconciliation " :
2015-03-27 15:38:31 +05:30
sle_id = make_entry ( sle , allow_negative_stock , via_landed_cost_voucher )
2014-04-07 12:02:57 +05:30
2013-09-26 16:16:44 +05:30
args = sle . copy ( )
args . update ( {
" sle_id " : sle_id ,
" is_amended " : is_amended
} )
2015-03-27 15:38:31 +05:30
update_bin ( args , allow_negative_stock , via_landed_cost_voucher )
2014-10-06 11:53:52 +05:30
2013-09-26 16:16:44 +05:30
if cancel :
2014-10-06 11:53:52 +05:30
delete_cancelled_entry ( sl_entries [ 0 ] . get ( ' voucher_type ' ) , sl_entries [ 0 ] . get ( ' voucher_no ' ) )
2014-04-07 12:02:57 +05:30
2013-08-20 15:37:33 +05:30
def set_as_cancel ( voucher_type , voucher_no ) :
2014-02-26 12:35:33 +05:30
frappe . db . sql ( """ update `tabStock Ledger Entry` set is_cancelled= ' Yes ' ,
2013-08-20 15:37:33 +05:30
modified = % s , modified_by = % s
2014-04-07 12:02:57 +05:30
where voucher_no = % s and voucher_type = % s """ ,
2014-02-14 15:47:51 +05:30
( now ( ) , frappe . session . user , voucher_type , voucher_no ) )
2014-04-07 12:02:57 +05:30
2015-03-27 15:38:31 +05:30
def make_entry ( args , allow_negative_stock = False , via_landed_cost_voucher = False ) :
2013-08-19 16:17:18 +05:30
args . update ( { " doctype " : " Stock Ledger Entry " } )
2014-04-04 12:16:26 +05:30
sle = frappe . get_doc ( args )
2015-02-10 14:41:27 +05:30
sle . flags . ignore_permissions = 1
2015-01-23 12:18:01 +05:30
sle . allow_negative_stock = allow_negative_stock
2015-03-27 15:38:31 +05:30
sle . via_landed_cost_voucher = via_landed_cost_voucher
2013-08-19 16:17:18 +05:30
sle . insert ( )
2013-08-23 15:17:36 +05:30
sle . submit ( )
2014-03-28 13:55:00 +05:30
return sle . name
2014-04-07 12:02:57 +05:30
2013-08-20 15:37:33 +05:30
def delete_cancelled_entry ( voucher_type , voucher_no ) :
2014-04-07 12:02:57 +05:30
frappe . db . sql ( """ delete from `tabStock Ledger Entry`
2013-08-20 15:37:33 +05:30
where voucher_type = % s and voucher_no = % s """ , (voucher_type, voucher_no))
2013-08-19 16:17:18 +05:30
2015-02-17 19:55:17 +05:30
class update_entries_after ( object ) :
2013-01-08 18:29:24 +05:30
"""
2014-04-07 12:02:57 +05:30
update valution rate and qty after transaction
2013-01-08 18:29:24 +05:30
from the current time - bucket onwards
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
: param args : args as dict
args = {
" item_code " : " ABC " ,
" warehouse " : " XYZ " ,
" posting_date " : " 2012-12-12 " ,
" posting_time " : " 12:00 "
}
2013-01-08 18:29:24 +05:30
"""
2015-04-06 12:59:34 +05:30
def __init__ ( self , args , allow_zero_rate = False , allow_negative_stock = None , via_landed_cost_voucher = False , verbose = 1 ) :
2015-02-17 19:55:17 +05:30
from frappe . model . meta import get_field_precision
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
self . exceptions = [ ]
self . verbose = verbose
self . allow_zero_rate = allow_zero_rate
self . allow_negative_stock = allow_negative_stock
2015-04-06 12:59:34 +05:30
self . via_landed_cost_voucher = via_landed_cost_voucher
2015-03-03 14:07:07 +05:30
if not self . allow_negative_stock :
2015-02-17 19:55:17 +05:30
self . allow_negative_stock = cint ( frappe . db . get_single_value ( " Stock Settings " ,
" allow_negative_stock " ) )
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
self . args = args
for key , value in args . iteritems ( ) :
setattr ( self , key , value )
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
self . previous_sle = self . get_sle_before_datetime ( )
self . previous_sle = self . previous_sle [ 0 ] if self . previous_sle else frappe . _dict ( )
2013-08-29 18:19:37 +05:30
2015-02-17 19:55:17 +05:30
for key in ( " qty_after_transaction " , " valuation_rate " , " stock_value " ) :
setattr ( self , key , flt ( self . previous_sle . get ( key ) ) )
2013-08-29 18:19:37 +05:30
2015-02-17 19:55:17 +05:30
self . company = frappe . db . get_value ( " Warehouse " , self . warehouse , " company " )
self . precision = get_field_precision ( frappe . get_meta ( " Stock Ledger Entry " ) . get_field ( " stock_value " ) ,
currency = frappe . db . get_value ( " Company " , self . company , " default_currency " ) )
2014-10-07 11:25:04 +05:30
2015-02-19 16:28:35 +05:30
self . prev_stock_value = self . previous_sle . stock_value or 0.0
2015-02-17 19:55:17 +05:30
self . stock_queue = json . loads ( self . previous_sle . stock_queue or " [] " )
self . valuation_method = get_valuation_method ( self . item_code )
self . stock_value_difference = 0.0
self . build ( )
2014-10-07 11:25:04 +05:30
2015-02-17 19:55:17 +05:30
def build ( self ) :
2015-02-19 20:05:45 +05:30
# includes current entry!
2015-02-17 19:55:17 +05:30
entries_to_fix = self . get_sle_after_datetime ( )
for sle in entries_to_fix :
self . process_sle ( sle )
if self . exceptions :
self . raise_exceptions ( )
self . update_bin ( )
def update_bin ( self ) :
# update bin
bin_name = frappe . db . get_value ( " Bin " , {
" item_code " : self . item_code ,
" warehouse " : self . warehouse
} )
if not bin_name :
bin_doc = frappe . get_doc ( {
" doctype " : " Bin " ,
" item_code " : self . item_code ,
" warehouse " : self . warehouse
} )
bin_doc . insert ( ignore_permissions = True )
2013-01-08 18:29:24 +05:30
else :
2015-02-17 19:55:17 +05:30
bin_doc = frappe . get_doc ( " Bin " , bin_name )
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
bin_doc . update ( {
" valuation_rate " : self . valuation_rate ,
" actual_qty " : self . qty_after_transaction ,
" stock_value " : self . stock_value
} )
bin_doc . save ( ignore_permissions = True )
2014-10-09 19:25:03 +05:30
2015-02-17 19:55:17 +05:30
def process_sle ( self , sle ) :
2015-04-06 12:59:34 +05:30
if ( sle . serial_no and not self . via_landed_cost_voucher ) or not cint ( self . allow_negative_stock ) :
2015-02-17 19:55:17 +05:30
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
if not self . validate_negative_stock ( sle ) :
self . qty_after_transaction + = flt ( sle . actual_qty )
return
2014-04-07 12:02:57 +05:30
2013-01-10 19:29:51 +05:30
if sle . serial_no :
2015-02-25 15:08:42 +05:30
self . get_serialized_values ( sle )
2015-02-17 19:55:17 +05:30
self . qty_after_transaction + = flt ( sle . actual_qty )
2015-02-18 20:22:59 +05:30
self . stock_value = flt ( self . qty_after_transaction ) * flt ( self . valuation_rate )
2013-01-08 18:29:24 +05:30
else :
2015-02-17 19:55:17 +05:30
if sle . voucher_type == " Stock Reconciliation " :
# assert
self . valuation_rate = sle . valuation_rate
self . qty_after_transaction = sle . qty_after_transaction
self . stock_queue = [ [ self . qty_after_transaction , self . valuation_rate ] ]
2015-02-18 20:22:59 +05:30
self . stock_value = flt ( self . qty_after_transaction ) * flt ( self . valuation_rate )
2015-02-17 19:55:17 +05:30
else :
if self . valuation_method == " Moving Average " :
self . get_moving_average_values ( sle )
self . qty_after_transaction + = flt ( sle . actual_qty )
2015-02-18 20:22:59 +05:30
self . stock_value = flt ( self . qty_after_transaction ) * flt ( self . valuation_rate )
2015-02-17 19:55:17 +05:30
else :
self . get_fifo_values ( sle )
self . qty_after_transaction + = flt ( sle . actual_qty )
self . stock_value = sum ( ( flt ( batch [ 0 ] ) * flt ( batch [ 1 ] ) for batch in self . stock_queue ) )
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
# rounding as per precision
self . stock_value = flt ( self . stock_value , self . precision )
2014-04-07 12:02:57 +05:30
2015-02-17 19:55:17 +05:30
stock_value_difference = self . stock_value - self . prev_stock_value
self . prev_stock_value = self . stock_value
2014-04-07 12:02:57 +05:30
2013-01-08 18:29:24 +05:30
# update current sle
2015-02-18 11:38:05 +05:30
sle . qty_after_transaction = self . qty_after_transaction
sle . valuation_rate = self . valuation_rate
2015-02-19 20:05:45 +05:30
sle . stock_value = self . stock_value
2015-02-18 11:38:05 +05:30
sle . stock_queue = json . dumps ( self . stock_queue )
sle . stock_value_difference = stock_value_difference
2015-02-18 20:22:59 +05:30
sle . doctype = " Stock Ledger Entry "
frappe . get_doc ( sle ) . db_update ( )
2015-02-17 19:55:17 +05:30
def validate_negative_stock ( self , sle ) :
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
diff = self . qty_after_transaction + flt ( sle . actual_qty )
if diff < 0 and abs ( diff ) > 0.0001 :
# negative stock!
exc = sle . copy ( ) . update ( { " diff " : diff } )
self . exceptions . append ( exc )
return False
2013-01-08 18:29:24 +05:30
else :
2015-02-17 19:55:17 +05:30
return True
def get_serialized_values ( self , sle ) :
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 = self . valuation_rate
2015-02-25 15:08:42 +05:30
elif incoming_rate == 0 :
if flt ( sle . actual_qty ) < 0 :
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
incoming_rate = flt ( frappe . db . sql ( """ select avg(ifnull(purchase_rate, 0))
from ` tabSerial No ` where name in ( % s ) """ % ( " , " .join([ " %s " ]*len(serial_no))),
tuple ( serial_no ) ) [ 0 ] [ 0 ] )
2015-02-17 19:55:17 +05:30
if incoming_rate and not self . valuation_rate :
self . valuation_rate = incoming_rate
else :
new_stock_qty = self . qty_after_transaction + actual_qty
if new_stock_qty > 0 :
new_stock_value = self . qty_after_transaction * self . 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
self . valuation_rate = new_stock_value / new_stock_qty
def get_moving_average_values ( self , sle ) :
incoming_rate = flt ( sle . incoming_rate )
actual_qty = flt ( sle . actual_qty )
if flt ( sle . actual_qty ) > 0 :
if self . qty_after_transaction < 0 and not self . valuation_rate :
# if negative stock, take current valuation rate as incoming rate
self . valuation_rate = incoming_rate
new_stock_qty = abs ( self . qty_after_transaction ) + actual_qty
new_stock_value = ( abs ( self . qty_after_transaction ) * self . valuation_rate ) + ( actual_qty * incoming_rate )
if new_stock_qty :
self . valuation_rate = new_stock_value / flt ( new_stock_qty )
elif not self . valuation_rate and self . qty_after_transaction < = 0 :
2015-02-22 22:32:39 +05:30
self . valuation_rate = get_valuation_rate ( sle . item_code , sle . warehouse , self . allow_zero_rate )
2015-02-17 19:55:17 +05:30
2015-02-22 22:32:39 +05:30
return abs ( flt ( self . valuation_rate ) )
2015-02-17 19:55:17 +05:30
def get_fifo_values ( self , sle ) :
incoming_rate = flt ( sle . incoming_rate )
actual_qty = flt ( sle . actual_qty )
if actual_qty > 0 :
if not self . stock_queue :
self . stock_queue . append ( [ 0 , 0 ] )
2015-02-19 20:05:45 +05:30
# last row has the same rate, just updated the qty
if self . stock_queue [ - 1 ] [ 1 ] == incoming_rate :
self . stock_queue [ - 1 ] [ 0 ] + = actual_qty
2013-01-08 18:29:24 +05:30
else :
2015-02-19 20:05:45 +05:30
if self . stock_queue [ - 1 ] [ 0 ] > 0 :
self . stock_queue . append ( [ actual_qty , incoming_rate ] )
2015-02-17 19:55:17 +05:30
else :
2015-02-19 20:05:45 +05:30
qty = self . stock_queue [ - 1 ] [ 0 ] + actual_qty
if qty == 0 :
self . stock_queue . pop ( - 1 )
else :
self . stock_queue [ - 1 ] = [ qty , incoming_rate ]
2015-02-17 19:55:17 +05:30
else :
qty_to_pop = abs ( actual_qty )
while qty_to_pop :
if not self . stock_queue :
if self . qty_after_transaction > 0 :
_rate = get_valuation_rate ( sle . item_code , sle . warehouse , self . allow_zero_rate )
else :
_rate = 0
self . stock_queue . append ( [ 0 , _rate ] )
batch = self . stock_queue [ 0 ]
if qty_to_pop > = batch [ 0 ] :
# consume current batch
qty_to_pop = qty_to_pop - batch [ 0 ]
self . stock_queue . pop ( 0 )
if not self . stock_queue and qty_to_pop :
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
self . stock_queue . append ( [ - qty_to_pop , batch [ 1 ] ] )
break
2014-10-09 19:25:03 +05:30
2015-02-17 19:55:17 +05:30
else :
# qty found in current batch
# consume it and exit
batch [ 0 ] = batch [ 0 ] - qty_to_pop
qty_to_pop = 0
stock_value = sum ( ( flt ( batch [ 0 ] ) * flt ( batch [ 1 ] ) for batch in self . stock_queue ) )
stock_qty = sum ( ( flt ( batch [ 0 ] ) for batch in self . stock_queue ) )
self . valuation_rate = ( stock_value / flt ( stock_qty ) ) if stock_qty else 0
def get_sle_before_datetime ( self ) :
""" get previous stock ledger entry before current time-bucket """
return get_stock_ledger_entries ( self . args , " < " , " desc " , " limit 1 " , for_update = False )
def get_sle_after_datetime ( self ) :
""" get Stock Ledger Entries after a particular datetime, for reposting """
2015-02-19 20:05:45 +05:30
return get_stock_ledger_entries ( self . previous_sle or frappe . _dict ( {
" item_code " : self . args . get ( " item_code " ) , " warehouse " : self . args . get ( " warehouse " ) } ) ,
" > " , " asc " , for_update = True )
2015-02-17 19:55:17 +05:30
def raise_exceptions ( self ) :
deficiency = min ( e [ " diff " ] for e in self . exceptions )
msg = _ ( " Negative Stock Error ( {6} ) for Item {0} in Warehouse {1} on {2} {3} in {4} {5} " ) . format ( self . item_code ,
self . warehouse , self . exceptions [ 0 ] [ " posting_date " ] , self . exceptions [ 0 ] [ " posting_time " ] ,
_ ( self . exceptions [ 0 ] [ " voucher_type " ] ) , self . exceptions [ 0 ] [ " voucher_no " ] , deficiency )
if self . verbose :
frappe . throw ( msg , NegativeStockError )
else :
raise NegativeStockError , msg
2014-04-07 12:02:57 +05:30
2013-01-11 11:44:49 +05:30
def get_previous_sle ( args , for_update = False ) :
2013-01-10 19:29:51 +05:30
"""
2014-04-07 12:02:57 +05:30
get the last sle on or before the current time - bucket ,
2013-01-10 19:29:51 +05:30
to get actual qty before transaction , this function
is called from various transaction like stock entry , reco etc
2014-04-07 12:02:57 +05:30
2013-01-10 19:29:51 +05:30
args = {
" item_code " : " ABC " ,
" warehouse " : " XYZ " ,
" posting_date " : " 2012-12-12 " ,
" posting_time " : " 12:00 " ,
" sle " : " name of reference Stock Ledger Entry "
}
"""
2015-02-17 19:55:17 +05:30
args [ " name " ] = args . get ( " sle " , None ) or " "
sle = get_stock_ledger_entries ( args , " <= " , " desc " , " limit 1 " , for_update = for_update )
2013-09-18 18:31:03 +05:30
return sle and sle [ 0 ] or { }
2014-10-15 11:34:40 +05:30
2015-02-19 20:05:45 +05:30
def get_stock_ledger_entries ( previous_sle , operator = None , order = " desc " , limit = None , for_update = False , debug = False ) :
2015-02-17 19:55:17 +05:30
""" get stock ledger entries filtered by specific posting datetime conditions """
conditions = " timestamp(posting_date, posting_time) {0} timestamp( %(posting_date)s , %(posting_time)s ) " . format ( operator )
if not previous_sle . get ( " posting_date " ) :
previous_sle [ " posting_date " ] = " 1900-01-01 "
if not previous_sle . get ( " posting_time " ) :
previous_sle [ " posting_time " ] = " 00:00 "
if operator in ( " > " , " <= " ) and previous_sle . get ( " name " ) :
conditions + = " and name!= %(name)s "
return frappe . db . sql ( """ select *, timestamp(posting_date, posting_time) as " timestamp " from `tabStock Ledger Entry`
where item_code = % % ( item_code ) s
and warehouse = % % ( warehouse ) s
and ifnull ( is_cancelled , ' No ' ) = ' No '
and % ( conditions ) s
order by timestamp ( posting_date , posting_time ) % ( order ) s , name % ( order ) s
% ( limit ) s % ( for_update ) s """ % {
" conditions " : conditions ,
" limit " : limit or " " ,
" for_update " : for_update and " for update " or " " ,
" order " : order
2015-02-19 20:05:45 +05:30
} , previous_sle , as_dict = 1 , debug = debug )
2015-02-17 19:55:17 +05:30
2014-10-15 12:23:35 +05:30
def get_valuation_rate ( item_code , warehouse , allow_zero_rate = False ) :
2014-10-15 11:34:40 +05:30
last_valuation_rate = frappe . db . sql ( """ select valuation_rate
from ` tabStock Ledger Entry `
where item_code = % s and warehouse = % s
and ifnull ( valuation_rate , 0 ) > 0
order by posting_date desc , posting_time desc , name desc limit 1 """ , (item_code, warehouse))
if not last_valuation_rate :
last_valuation_rate = frappe . db . sql ( """ select valuation_rate
from ` tabStock Ledger Entry `
where item_code = % s and ifnull ( valuation_rate , 0 ) > 0
order by posting_date desc , posting_time desc , name desc limit 1 """ , item_code)
valuation_rate = flt ( last_valuation_rate [ 0 ] [ 0 ] ) if last_valuation_rate else 0
if not valuation_rate :
valuation_rate = frappe . db . get_value ( " Item Price " , { " item_code " : item_code , " buying " : 1 } , " price_list_rate " )
2014-10-15 12:23:35 +05:30
if not allow_zero_rate and not valuation_rate and cint ( frappe . db . get_value ( " Accounts Settings " , None , " auto_accounting_for_stock " ) ) :
2014-10-15 11:34:40 +05:30
frappe . throw ( _ ( " Purchase rate for item: {0} not found, which is required to book accounting entry (expense). Please mention item price against a buying price list. " ) . format ( item_code ) )
return valuation_rate