2015-03-03 09:25:30 +00:00
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
2013-08-05 09:29:54 +00:00
# License: GNU General Public License v3. See license.txt
2013-01-08 12:59:24 +00:00
2021-04-24 11:58:33 +00:00
import copy
2013-01-09 09:53:05 +00:00
import json
2022-02-12 07:38:28 +00:00
from typing import Optional
2021-09-02 11:14:59 +00:00
2014-04-14 13:50:45 +00:00
import frappe
from frappe import _
2020-12-21 09:15:50 +00:00
from frappe . model . meta import get_field_precision
2022-02-19 10:21:04 +00:00
from frappe . query_builder . functions import Sum
2021-12-03 06:48:59 +00:00
from frappe . utils import cint , cstr , flt , get_link_to_form , getdate , now , nowdate
2022-02-19 10:21:04 +00:00
from pypika import CustomFunction
2018-02-14 11:38:59 +00:00
2021-04-24 11:58:33 +00:00
import erpnext
2021-12-03 06:48:59 +00:00
from erpnext . stock . doctype . bin . bin import update_qty as update_bin_qty
2020-12-21 09:15:50 +00:00
from erpnext . stock . utils import (
get_incoming_outgoing_rate_for_cancel ,
2021-10-12 14:45:55 +00:00
get_or_make_bin ,
2020-12-21 09:15:50 +00:00
get_valuation_method ,
2021-09-02 11:14:59 +00:00
)
2022-02-19 15:28:36 +00:00
from erpnext . stock . valuation import FIFOValuation , LIFOValuation , round_off_if_near_zero
2021-09-02 11:14:59 +00:00
2021-07-12 07:54:43 +00:00
2014-02-14 10:17:51 +00:00
class NegativeStockError ( frappe . ValidationError ) : pass
2021-04-24 11:58:33 +00:00
class SerialNoExistsInFutureTransaction ( frappe . ValidationError ) :
pass
2013-01-08 12:59:24 +00:00
2013-09-25 14:25:41 +00:00
2020-04-30 05:08:58 +00:00
def make_sl_entries ( sl_entries , allow_negative_stock = False , via_landed_cost_voucher = False ) :
2021-06-15 04:51:44 +00:00
from erpnext . controllers . stock_controller import future_sle_exists
2013-09-26 10:46:44 +00:00
if sl_entries :
2020-04-30 05:08:58 +00:00
cancel = sl_entries [ 0 ] . get ( " is_cancelled " )
2013-09-26 10:46:44 +00:00
if cancel :
2021-02-18 08:44:21 +00:00
validate_cancellation ( sl_entries )
2020-04-30 05:08:58 +00:00
set_as_cancel ( sl_entries [ 0 ] . get ( ' voucher_type ' ) , sl_entries [ 0 ] . get ( ' voucher_no ' ) )
2014-04-07 06:32:57 +00:00
2021-06-15 04:51:44 +00:00
args = get_args_for_future_sle ( sl_entries [ 0 ] )
future_sle_exists ( args , sl_entries )
2013-09-26 10:46:44 +00:00
for sle in sl_entries :
2021-04-24 11:58:33 +00:00
if sle . serial_no :
validate_serial_no ( sle )
2020-12-21 09:15:50 +00:00
if cancel :
sle [ ' actual_qty ' ] = - flt ( sle . get ( ' actual_qty ' ) )
2020-04-30 05:08:58 +00:00
2020-12-21 09:15:50 +00:00
if sle [ ' actual_qty ' ] < 0 and not sle . get ( ' outgoing_rate ' ) :
sle [ ' outgoing_rate ' ] = get_incoming_outgoing_rate_for_cancel ( sle . item_code ,
sle . voucher_type , sle . voucher_no , sle . voucher_detail_no )
sle [ ' incoming_rate ' ] = 0.0
2020-04-30 05:08:58 +00:00
2020-12-21 09:15:50 +00:00
if sle [ ' actual_qty ' ] > 0 and not sle . get ( ' incoming_rate ' ) :
sle [ ' incoming_rate ' ] = get_incoming_outgoing_rate_for_cancel ( sle . item_code ,
sle . voucher_type , sle . voucher_no , sle . voucher_detail_no )
sle [ ' outgoing_rate ' ] = 0.0
2014-04-07 06:32:57 +00:00
2014-11-03 09:38:21 +00:00
if sle . get ( " actual_qty " ) or sle . get ( " voucher_type " ) == " Stock Reconciliation " :
2020-12-21 09:15:50 +00:00
sle_doc = make_entry ( sle , allow_negative_stock , via_landed_cost_voucher )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
args = sle_doc . as_dict ( )
2021-07-02 11:43:45 +00:00
if sle . get ( " voucher_type " ) == " Stock Reconciliation " :
# preserve previous_qty_after_transaction for qty reposting
args . previous_qty_after_transaction = sle . get ( " previous_qty_after_transaction " )
2021-12-03 06:48:59 +00:00
is_stock_item = frappe . get_cached_value ( ' Item ' , args . get ( " item_code " ) , ' is_stock_item ' )
if is_stock_item :
bin_name = get_or_make_bin ( args . get ( " item_code " ) , args . get ( " warehouse " ) )
repost_current_voucher ( args , allow_negative_stock , via_landed_cost_voucher )
2021-12-20 09:37:41 +00:00
update_bin_qty ( bin_name , args )
2021-12-03 06:48:59 +00:00
else :
frappe . msgprint ( _ ( " Item {0} ignored since it is not a stock item " ) . format ( args . get ( " item_code " ) ) )
def repost_current_voucher ( args , allow_negative_stock = False , via_landed_cost_voucher = False ) :
if args . get ( " actual_qty " ) or args . get ( " voucher_type " ) == " Stock Reconciliation " :
if not args . get ( " posting_date " ) :
args [ " posting_date " ] = nowdate ( )
if args . get ( " is_cancelled " ) and via_landed_cost_voucher :
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after ( {
" item_code " : args . get ( ' item_code ' ) ,
" warehouse " : args . get ( ' warehouse ' ) ,
" posting_date " : args . get ( " posting_date " ) ,
" posting_time " : args . get ( " posting_time " ) ,
" voucher_type " : args . get ( " voucher_type " ) ,
" voucher_no " : args . get ( " voucher_no " ) ,
" sle_id " : args . get ( ' name ' ) ,
" creation " : args . get ( ' creation ' )
} , allow_negative_stock = allow_negative_stock , via_landed_cost_voucher = via_landed_cost_voucher )
# update qty in future sle and Validate negative qty
update_qty_in_future_sle ( args , allow_negative_stock )
2014-10-06 06:23:52 +00:00
2021-06-15 04:51:44 +00:00
def get_args_for_future_sle ( row ) :
return frappe . _dict ( {
' voucher_type ' : row . get ( ' voucher_type ' ) ,
' voucher_no ' : row . get ( ' voucher_no ' ) ,
' posting_date ' : row . get ( ' posting_date ' ) ,
' posting_time ' : row . get ( ' posting_time ' )
} )
2021-04-24 11:58:33 +00:00
def validate_serial_no ( sle ) :
from erpnext . stock . doctype . serial_no . serial_no import get_serial_nos
2022-01-16 15:15:59 +00:00
2021-04-24 11:58:33 +00:00
for sn in get_serial_nos ( sle . serial_no ) :
args = copy . deepcopy ( sle )
args . serial_no = sn
args . warehouse = ' '
vouchers = [ ]
for row in get_stock_ledger_entries ( args , ' > ' ) :
voucher_type = frappe . bold ( row . voucher_type )
voucher_no = frappe . bold ( get_link_to_form ( row . voucher_type , row . voucher_no ) )
vouchers . append ( f ' { voucher_type } { voucher_no } ' )
if vouchers :
serial_no = frappe . bold ( sn )
msg = ( f ''' The serial no { serial_no } has been used in the future transactions so you need to cancel them first.
The list of the transactions are as below . ''' + ' <br><br><ul><li> ' )
msg + = ' </li><li> ' . join ( vouchers )
msg + = ' </li></ul> '
title = ' Cannot Submit ' if not sle . get ( ' is_cancelled ' ) else ' Cannot Cancel '
frappe . throw ( _ ( msg ) , title = _ ( title ) , exc = SerialNoExistsInFutureTransaction )
2021-02-18 08:44:21 +00:00
def validate_cancellation ( args ) :
if args [ 0 ] . get ( " is_cancelled " ) :
repost_entry = frappe . db . get_value ( " Repost Item Valuation " , {
' voucher_type ' : args [ 0 ] . voucher_type ,
' voucher_no ' : args [ 0 ] . voucher_no ,
' docstatus ' : 1
} , [ ' name ' , ' status ' ] , as_dict = 1 )
if repost_entry :
if repost_entry . status == ' In Progress ' :
frappe . throw ( _ ( " Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet. " ) )
if repost_entry . status == ' Queued ' :
2021-02-23 11:08:52 +00:00
doc = frappe . get_doc ( " Repost Item Valuation " , repost_entry . name )
2021-11-18 07:21:26 +00:00
doc . flags . ignore_permissions = True
2021-02-23 11:08:52 +00:00
doc . cancel ( )
doc . delete ( )
2014-04-07 06:32:57 +00:00
2013-08-20 10:07:33 +00:00
def set_as_cancel ( voucher_type , voucher_no ) :
2020-04-30 05:08:58 +00:00
frappe . db . sql ( """ update `tabStock Ledger Entry` set is_cancelled=1,
2013-08-20 10:07:33 +00:00
modified = % s , modified_by = % s
2020-04-30 05:08:58 +00:00
where voucher_type = % s and voucher_no = % s and is_cancelled = 0 """ ,
2014-02-14 10:17:51 +00:00
( now ( ) , frappe . session . user , voucher_type , voucher_no ) )
2014-04-07 06:32:57 +00:00
2015-03-27 10:08:31 +00:00
def make_entry ( args , allow_negative_stock = False , via_landed_cost_voucher = False ) :
2021-10-12 08:00:40 +00:00
args [ " doctype " ] = " Stock Ledger Entry "
2014-04-04 06:46:26 +00:00
sle = frappe . get_doc ( args )
2015-02-10 09:11:27 +00:00
sle . flags . ignore_permissions = 1
2015-01-23 06:48:01 +00:00
sle . allow_negative_stock = allow_negative_stock
2015-03-27 10:08:31 +00:00
sle . via_landed_cost_voucher = via_landed_cost_voucher
2013-08-23 09:47:36 +00:00
sle . submit ( )
2020-12-21 09:15:50 +00:00
return sle
2021-08-02 05:31:30 +00:00
def repost_future_sle ( args = None , voucher_type = None , voucher_no = None , allow_negative_stock = None , via_landed_cost_voucher = False , doc = None ) :
2020-12-21 09:15:50 +00:00
if not args and voucher_type and voucher_no :
2021-08-02 05:31:30 +00:00
args = get_items_to_be_repost ( voucher_type , voucher_no , doc )
2021-01-28 07:39:56 +00:00
2021-08-02 05:31:30 +00:00
distinct_item_warehouses = get_distinct_item_warehouse ( args , doc )
i = get_current_index ( doc ) or 0
2020-12-21 09:15:50 +00:00
while i < len ( args ) :
2021-08-02 05:31:30 +00:00
validate_item_warehouse ( args [ i ] )
2020-12-21 09:15:50 +00:00
obj = update_entries_after ( {
2021-08-02 05:31:30 +00:00
' item_code ' : args [ i ] . get ( ' item_code ' ) ,
' warehouse ' : args [ i ] . get ( ' warehouse ' ) ,
' posting_date ' : args [ i ] . get ( ' posting_date ' ) ,
' posting_time ' : args [ i ] . get ( ' posting_time ' ) ,
' creation ' : args [ i ] . get ( ' creation ' ) ,
' distinct_item_warehouses ' : distinct_item_warehouses
2020-12-21 09:15:50 +00:00
} , allow_negative_stock = allow_negative_stock , via_landed_cost_voucher = via_landed_cost_voucher )
2021-08-02 05:31:30 +00:00
distinct_item_warehouses [ ( args [ i ] . get ( ' item_code ' ) , args [ i ] . get ( ' warehouse ' ) ) ] . reposting_status = True
2021-07-12 07:54:43 +00:00
if obj . new_items_found :
for item_wh , data in distinct_item_warehouses . items ( ) :
if ( ' args_idx ' not in data and not data . reposting_status ) or ( data . sle_changed and data . reposting_status ) :
data . args_idx = len ( args )
args . append ( data . sle )
elif data . sle_changed and not data . reposting_status :
args [ data . args_idx ] = data . sle
2021-08-02 05:31:30 +00:00
2021-07-12 07:54:43 +00:00
data . sle_changed = False
2020-12-21 09:15:50 +00:00
i + = 1
2021-08-02 05:31:30 +00:00
if doc and i % 2 == 0 :
update_args_in_repost_item_valuation ( doc , i , args , distinct_item_warehouses )
if doc and args :
update_args_in_repost_item_valuation ( doc , i , args , distinct_item_warehouses )
def validate_item_warehouse ( args ) :
for field in [ ' item_code ' , ' warehouse ' , ' posting_date ' , ' posting_time ' ] :
if not args . get ( field ) :
validation_msg = f ' The field { frappe . unscrub ( args . get ( field ) ) } is required for the reposting '
frappe . throw ( _ ( validation_msg ) )
def update_args_in_repost_item_valuation ( doc , index , args , distinct_item_warehouses ) :
frappe . db . set_value ( doc . doctype , doc . name , {
' items_to_be_repost ' : json . dumps ( args , default = str ) ,
' distinct_item_and_warehouse ' : json . dumps ( { str ( k ) : v for k , v in distinct_item_warehouses . items ( ) } , default = str ) ,
' current_index ' : index
} )
frappe . db . commit ( )
frappe . publish_realtime ( ' item_reposting_progress ' , {
' name ' : doc . name ,
' items_to_be_repost ' : json . dumps ( args , default = str ) ,
' current_index ' : index
} )
def get_items_to_be_repost ( voucher_type , voucher_no , doc = None ) :
if doc and doc . items_to_be_repost :
return json . loads ( doc . items_to_be_repost ) or [ ]
2020-12-21 09:15:50 +00:00
return frappe . db . get_all ( " Stock Ledger Entry " ,
filters = { " voucher_type " : voucher_type , " voucher_no " : voucher_no } ,
2021-02-18 08:44:21 +00:00
fields = [ " item_code " , " warehouse " , " posting_date " , " posting_time " , " creation " ] ,
2020-12-21 09:15:50 +00:00
order_by = " creation asc " ,
group_by = " item_code, warehouse "
)
2013-08-19 10:47:18 +00:00
2021-08-02 05:31:30 +00:00
def get_distinct_item_warehouse ( args = None , doc = None ) :
distinct_item_warehouses = { }
if doc and doc . distinct_item_and_warehouse :
distinct_item_warehouses = json . loads ( doc . distinct_item_and_warehouse )
distinct_item_warehouses = { frappe . safe_eval ( k ) : frappe . _dict ( v ) for k , v in distinct_item_warehouses . items ( ) }
else :
for i , d in enumerate ( args ) :
distinct_item_warehouses . setdefault ( ( d . item_code , d . warehouse ) , frappe . _dict ( {
" reposting_status " : False ,
" sle " : d ,
" args_idx " : i
} ) )
return distinct_item_warehouses
def get_current_index ( doc = None ) :
if doc and doc . current_index :
return doc . current_index
2015-02-17 14:25:17 +00:00
class update_entries_after ( object ) :
2013-01-08 12:59:24 +00:00
"""
2014-04-07 06:32:57 +00:00
update valution rate and qty after transaction
2013-01-08 12:59:24 +00:00
from the current time - bucket onwards
2014-04-07 06:32:57 +00:00
2015-02-17 14:25:17 +00:00
: param args : args as dict
args = {
" item_code " : " ABC " ,
" warehouse " : " XYZ " ,
" posting_date " : " 2012-12-12 " ,
" posting_time " : " 12:00 "
}
2013-01-08 12:59:24 +00:00
"""
2015-04-06 07:29:34 +00:00
def __init__ ( self , args , allow_zero_rate = False , allow_negative_stock = None , via_landed_cost_voucher = False , verbose = 1 ) :
2020-12-21 09:15:50 +00:00
self . exceptions = { }
2015-02-17 14:25:17 +00:00
self . verbose = verbose
self . allow_zero_rate = allow_zero_rate
2015-04-06 07:29:34 +00:00
self . via_landed_cost_voucher = via_landed_cost_voucher
2022-02-12 07:38:28 +00:00
self . item_code = args . get ( " item_code " )
self . allow_negative_stock = allow_negative_stock or is_negative_stock_allowed ( item_code = self . item_code )
2014-04-07 06:32:57 +00:00
2020-12-21 09:15:50 +00:00
self . args = frappe . _dict ( args )
if self . args . sle_id :
self . args [ ' name ' ] = self . args . sle_id
2021-02-23 11:08:52 +00:00
2020-12-21 09:15:50 +00:00
self . company = frappe . get_cached_value ( " Warehouse " , self . args . warehouse , " company " )
self . get_precision ( )
self . valuation_method = get_valuation_method ( self . item_code )
2021-07-12 07:54:43 +00:00
self . new_items_found = False
self . distinct_item_warehouses = args . get ( " distinct_item_warehouses " , frappe . _dict ( ) )
2013-08-29 12:49:37 +00:00
2020-12-21 09:15:50 +00:00
self . data = frappe . _dict ( )
self . initialize_previous_data ( self . args )
self . build ( )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
def get_precision ( self ) :
company_base_currency = frappe . get_cached_value ( ' Company ' , self . company , " default_currency " )
2015-02-17 14:25:17 +00:00
self . precision = get_field_precision ( frappe . get_meta ( " Stock Ledger Entry " ) . get_field ( " stock_value " ) ,
2020-12-21 09:15:50 +00:00
currency = company_base_currency )
2014-10-07 05:55:04 +00:00
2020-12-21 09:15:50 +00:00
def initialize_previous_data ( self , args ) :
"""
Get previous sl entries for current item for each related warehouse
and assigns into self . data dict
: Data Structure :
self . data = {
warehouse1 : {
' previus_sle ' : { } ,
' qty_after_transaction ' : 10 ,
' valuation_rate ' : 100 ,
' stock_value ' : 1000 ,
' prev_stock_value ' : 1000 ,
' stock_queue ' : ' [[10, 100]] ' ,
' stock_value_difference ' : 1000
}
}
2014-10-07 05:55:04 +00:00
2020-12-21 09:15:50 +00:00
"""
2021-08-31 14:13:42 +00:00
self . data . setdefault ( args . warehouse , frappe . _dict ( ) )
warehouse_dict = self . data [ args . warehouse ]
2021-06-22 16:05:25 +00:00
previous_sle = get_previous_sle_of_current_voucher ( args )
2021-08-31 14:13:42 +00:00
warehouse_dict . previous_sle = previous_sle
2020-12-21 09:15:50 +00:00
2021-08-31 14:13:42 +00:00
for key in ( " qty_after_transaction " , " valuation_rate " , " stock_value " ) :
setattr ( warehouse_dict , key , flt ( previous_sle . get ( key ) ) )
warehouse_dict . update ( {
2020-12-21 09:15:50 +00:00
" prev_stock_value " : previous_sle . stock_value or 0.0 ,
" stock_queue " : json . loads ( previous_sle . stock_queue or " [] " ) ,
" stock_value_difference " : 0.0
} )
def build ( self ) :
2021-03-31 07:14:03 +00:00
from erpnext . controllers . stock_controller import future_sle_exists
2021-02-18 08:44:21 +00:00
2020-12-21 09:15:50 +00:00
if self . args . get ( " sle_id " ) :
2021-02-18 08:44:21 +00:00
self . process_sle_against_current_timestamp ( )
2021-03-31 07:14:03 +00:00
if not future_sle_exists ( self . args ) :
2021-02-18 08:44:21 +00:00
self . update_bin ( )
2020-04-30 05:08:58 +00:00
else :
2020-12-21 09:15:50 +00:00
entries_to_fix = self . get_future_entries_to_fix ( )
i = 0
while i < len ( entries_to_fix ) :
sle = entries_to_fix [ i ]
i + = 1
2020-04-30 05:08:58 +00:00
self . process_sle ( sle )
2015-02-17 14:25:17 +00:00
2020-12-21 09:15:50 +00:00
if sle . dependant_sle_voucher_detail_no :
2021-02-02 11:25:13 +00:00
entries_to_fix = self . get_dependent_entries_to_fix ( entries_to_fix , sle )
2021-02-23 11:08:52 +00:00
2021-02-18 08:44:21 +00:00
self . update_bin ( )
2020-12-21 09:15:50 +00:00
2015-02-17 14:25:17 +00:00
if self . exceptions :
self . raise_exceptions ( )
2021-02-18 08:44:21 +00:00
def process_sle_against_current_timestamp ( self ) :
2020-12-21 09:15:50 +00:00
sl_entries = self . get_sle_against_current_voucher ( )
for sle in sl_entries :
self . process_sle ( sle )
2016-06-12 05:33:00 +00:00
2020-12-21 09:15:50 +00:00
def get_sle_against_current_voucher ( self ) :
2021-02-15 13:57:49 +00:00
self . args [ ' time_format ' ] = ' % H: %i : %s '
2020-12-21 09:15:50 +00:00
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
2021-08-26 07:22:36 +00:00
and is_cancelled = 0
2021-02-18 08:44:21 +00:00
and timestamp ( posting_date , time_format ( posting_time , % ( time_format ) s ) ) = timestamp ( % ( posting_date ) s , time_format ( % ( posting_time ) s , % ( time_format ) s ) )
2020-12-21 09:15:50 +00:00
order by
creation ASC
for update
""" , self.args, as_dict=1)
def get_future_entries_to_fix ( self ) :
# includes current entry!
args = self . data [ self . args . warehouse ] . previous_sle \
or frappe . _dict ( { " item_code " : self . item_code , " warehouse " : self . args . warehouse } )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
return list ( self . get_sle_after_datetime ( args ) )
def get_dependent_entries_to_fix ( self , entries_to_fix , sle ) :
dependant_sle = get_sle_by_voucher_detail_no ( sle . dependant_sle_voucher_detail_no ,
excluded_sle = sle . name )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
if not dependant_sle :
2021-02-02 11:25:13 +00:00
return entries_to_fix
2020-12-21 09:15:50 +00:00
elif dependant_sle . item_code == self . item_code and dependant_sle . warehouse == self . args . warehouse :
2021-02-02 11:25:13 +00:00
return entries_to_fix
elif dependant_sle . item_code != self . item_code :
2021-07-12 07:54:43 +00:00
self . update_distinct_item_warehouses ( dependant_sle )
2021-02-02 11:25:13 +00:00
return entries_to_fix
elif dependant_sle . item_code == self . item_code and dependant_sle . warehouse in self . data :
return entries_to_fix
2021-07-12 07:54:43 +00:00
else :
return self . append_future_sle_for_dependant ( dependant_sle , entries_to_fix )
def update_distinct_item_warehouses ( self , dependant_sle ) :
key = ( dependant_sle . item_code , dependant_sle . warehouse )
val = frappe . _dict ( {
" sle " : dependant_sle
} )
if key not in self . distinct_item_warehouses :
self . distinct_item_warehouses [ key ] = val
self . new_items_found = True
else :
existing_sle_posting_date = self . distinct_item_warehouses [ key ] . get ( " sle " , { } ) . get ( " posting_date " )
if getdate ( dependant_sle . posting_date ) < getdate ( existing_sle_posting_date ) :
val . sle_changed = True
self . distinct_item_warehouses [ key ] = val
self . new_items_found = True
def append_future_sle_for_dependant ( self , dependant_sle , entries_to_fix ) :
2020-12-21 09:15:50 +00:00
self . initialize_previous_data ( dependant_sle )
args = self . data [ dependant_sle . warehouse ] . previous_sle \
or frappe . _dict ( { " item_code " : self . item_code , " warehouse " : dependant_sle . warehouse } )
future_sle_for_dependant = list ( self . get_sle_after_datetime ( args ) )
entries_to_fix . extend ( future_sle_for_dependant )
2021-02-02 11:25:13 +00:00
return sorted ( entries_to_fix , key = lambda k : k [ ' timestamp ' ] )
2014-10-09 13:55:03 +00:00
2015-02-17 14:25:17 +00:00
def process_sle ( self , sle ) :
2022-01-16 15:15:59 +00:00
from erpnext . stock . doctype . serial_no . serial_no import get_serial_nos
2020-12-21 09:15:50 +00:00
# previous sle data for this warehouse
self . wh_data = self . data [ sle . warehouse ]
2015-04-06 07:29:34 +00:00
if ( sle . serial_no and not self . via_landed_cost_voucher ) or not cint ( self . allow_negative_stock ) :
2015-02-17 14:25:17 +00:00
# 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 ) :
2020-12-21 09:15:50 +00:00
self . wh_data . qty_after_transaction + = flt ( sle . actual_qty )
2015-02-17 14:25:17 +00:00
return
2014-04-07 06:32:57 +00:00
2020-12-21 09:15:50 +00:00
# Get dynamic incoming/outgoing rate
2021-09-15 15:12:47 +00:00
if not self . args . get ( " sle_id " ) :
self . get_dynamic_incoming_outgoing_rate ( sle )
2021-01-28 07:39:56 +00:00
2022-01-16 15:15:59 +00:00
if get_serial_nos ( sle . serial_no ) :
2015-02-25 09:38:42 +00:00
self . get_serialized_values ( sle )
2020-12-21 09:15:50 +00:00
self . wh_data . qty_after_transaction + = flt ( sle . actual_qty )
2019-04-28 13:09:18 +00:00
if sle . voucher_type == " Stock Reconciliation " :
2020-12-21 09:15:50 +00:00
self . wh_data . qty_after_transaction = sle . qty_after_transaction
2019-04-28 13:09:18 +00:00
2020-12-21 09:15:50 +00:00
self . wh_data . stock_value = flt ( self . wh_data . qty_after_transaction ) * flt ( self . wh_data . valuation_rate )
2022-02-15 06:11:41 +00:00
elif sle . batch_no and frappe . db . get_value ( " Batch " , sle . batch_no , " use_batchwise_valuation " , cache = True ) :
self . update_batched_values ( sle )
2013-01-08 12:59:24 +00:00
else :
2019-05-24 11:23:51 +00:00
if sle . voucher_type == " Stock Reconciliation " and not sle . batch_no :
2015-02-17 14:25:17 +00:00
# assert
2020-12-21 09:15:50 +00:00
self . wh_data . valuation_rate = sle . valuation_rate
self . wh_data . qty_after_transaction = sle . qty_after_transaction
self . wh_data . stock_value = flt ( self . wh_data . qty_after_transaction ) * flt ( self . wh_data . valuation_rate )
2022-01-16 07:32:23 +00:00
if self . valuation_method != " Moving Average " :
self . wh_data . stock_queue = [ [ self . wh_data . qty_after_transaction , self . wh_data . valuation_rate ] ]
2015-02-17 14:25:17 +00:00
else :
if self . valuation_method == " Moving Average " :
self . get_moving_average_values ( sle )
2020-12-21 09:15:50 +00:00
self . wh_data . qty_after_transaction + = flt ( sle . actual_qty )
self . wh_data . stock_value = flt ( self . wh_data . qty_after_transaction ) * flt ( self . wh_data . valuation_rate )
2015-02-17 14:25:17 +00:00
else :
2022-02-02 07:21:21 +00:00
self . update_queue_values ( sle )
2014-04-07 06:32:57 +00:00
2015-02-17 14:25:17 +00:00
# rounding as per precision
2020-12-21 09:15:50 +00:00
self . wh_data . stock_value = flt ( self . wh_data . stock_value , self . precision )
2022-02-20 06:05:53 +00:00
if not self . wh_data . qty_after_transaction :
self . wh_data . stock_value = 0.0
2020-12-21 09:15:50 +00:00
stock_value_difference = self . wh_data . stock_value - self . wh_data . prev_stock_value
self . wh_data . prev_stock_value = self . wh_data . stock_value
2014-04-07 06:32:57 +00:00
2013-01-08 12:59:24 +00:00
# update current sle
2020-12-21 09:15:50 +00:00
sle . qty_after_transaction = self . wh_data . qty_after_transaction
sle . valuation_rate = self . wh_data . valuation_rate
sle . stock_value = self . wh_data . stock_value
sle . stock_queue = json . dumps ( self . wh_data . stock_queue )
2015-02-18 06:08:05 +00:00
sle . stock_value_difference = stock_value_difference
2015-02-18 14:52:59 +00:00
sle . doctype = " Stock Ledger Entry "
frappe . get_doc ( sle ) . db_update ( )
2015-02-17 14:25:17 +00:00
2021-09-15 15:12:47 +00:00
if not self . args . get ( " sle_id " ) :
self . update_outgoing_rate_on_transaction ( sle )
2020-12-21 09:15:50 +00:00
2022-02-15 06:11:41 +00:00
2015-02-17 14:25:17 +00:00
def validate_negative_stock ( self , sle ) :
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
2020-12-21 09:15:50 +00:00
diff = self . wh_data . qty_after_transaction + flt ( sle . actual_qty )
2015-02-17 14:25:17 +00:00
if diff < 0 and abs ( diff ) > 0.0001 :
# negative stock!
exc = sle . copy ( ) . update ( { " diff " : diff } )
2020-12-21 09:15:50 +00:00
self . exceptions . setdefault ( sle . warehouse , [ ] ) . append ( exc )
2015-02-17 14:25:17 +00:00
return False
2013-01-08 12:59:24 +00:00
else :
2015-02-17 14:25:17 +00:00
return True
2020-12-21 09:15:50 +00:00
def get_dynamic_incoming_outgoing_rate ( self , sle ) :
# Get updated incoming/outgoing rate from transaction
if sle . recalculate_rate :
rate = self . get_incoming_outgoing_rate_from_transaction ( sle )
if flt ( sle . actual_qty ) > = 0 :
sle . incoming_rate = rate
else :
sle . outgoing_rate = rate
def get_incoming_outgoing_rate_from_transaction ( self , sle ) :
rate = 0
# Material Transfer, Repack, Manufacturing
if sle . voucher_type == " Stock Entry " :
2021-07-12 07:54:43 +00:00
self . recalculate_amounts_in_stock_entry ( sle . voucher_no )
2020-12-21 09:15:50 +00:00
rate = frappe . db . get_value ( " Stock Entry Detail " , sle . voucher_detail_no , " valuation_rate " )
# Sales and Purchase Return
elif sle . voucher_type in ( " Purchase Receipt " , " Purchase Invoice " , " Delivery Note " , " Sales Invoice " ) :
if frappe . get_cached_value ( sle . voucher_type , sle . voucher_no , " is_return " ) :
from erpnext . controllers . sales_and_purchase_return import (
get_rate_for_return , # don't move this import to top
2021-09-02 11:14:59 +00:00
)
2021-04-13 15:25:52 +00:00
rate = get_rate_for_return ( sle . voucher_type , sle . voucher_no , sle . item_code ,
voucher_detail_no = sle . voucher_detail_no , sle = sle )
2020-12-21 09:15:50 +00:00
else :
if sle . voucher_type in ( " Purchase Receipt " , " Purchase Invoice " ) :
2021-01-28 07:39:56 +00:00
rate_field = " valuation_rate "
2020-12-21 09:15:50 +00:00
else :
rate_field = " incoming_rate "
# check in item table
item_code , incoming_rate = frappe . db . get_value ( sle . voucher_type + " Item " ,
sle . voucher_detail_no , [ " item_code " , rate_field ] )
if item_code == sle . item_code :
rate = incoming_rate
else :
if sle . voucher_type in ( " Delivery Note " , " Sales Invoice " ) :
ref_doctype = " Packed Item "
else :
ref_doctype = " Purchase Receipt Item Supplied "
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
rate = frappe . db . get_value ( ref_doctype , { " parent_detail_docname " : sle . voucher_detail_no ,
" item_code " : sle . item_code } , rate_field )
return rate
def update_outgoing_rate_on_transaction ( self , sle ) :
"""
Update outgoing rate in Stock Entry , Delivery Note , Sales Invoice and Sales Return
In case of Stock Entry , also calculate FG Item rate and total incoming / outgoing amount
"""
if sle . actual_qty and sle . voucher_detail_no :
outgoing_rate = abs ( flt ( sle . stock_value_difference ) ) / abs ( sle . actual_qty )
if flt ( sle . actual_qty ) < 0 and sle . voucher_type == " Stock Entry " :
self . update_rate_on_stock_entry ( sle , outgoing_rate )
elif sle . voucher_type in ( " Delivery Note " , " Sales Invoice " ) :
self . update_rate_on_delivery_and_sales_return ( sle , outgoing_rate )
elif flt ( sle . actual_qty ) < 0 and sle . voucher_type in ( " Purchase Receipt " , " Purchase Invoice " ) :
self . update_rate_on_purchase_receipt ( sle , outgoing_rate )
def update_rate_on_stock_entry ( self , sle , outgoing_rate ) :
frappe . db . set_value ( " Stock Entry Detail " , sle . voucher_detail_no , " basic_rate " , outgoing_rate )
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
2021-07-12 07:54:43 +00:00
if not sle . dependant_sle_voucher_detail_no :
self . recalculate_amounts_in_stock_entry ( sle . voucher_no )
def recalculate_amounts_in_stock_entry ( self , voucher_no ) :
stock_entry = frappe . get_doc ( " Stock Entry " , voucher_no , for_update = True )
2020-12-21 09:15:50 +00:00
stock_entry . calculate_rate_and_amount ( reset_outgoing_rate = False , raise_error_if_no_rate = False )
stock_entry . db_update ( )
for d in stock_entry . items :
d . db_update ( )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
def update_rate_on_delivery_and_sales_return ( self , sle , outgoing_rate ) :
# Update item's incoming rate on transaction
item_code = frappe . db . get_value ( sle . voucher_type + " Item " , sle . voucher_detail_no , " item_code " )
if item_code == sle . item_code :
frappe . db . set_value ( sle . voucher_type + " Item " , sle . voucher_detail_no , " incoming_rate " , outgoing_rate )
else :
# packed item
frappe . db . set_value ( " Packed Item " ,
{ " parent_detail_docname " : sle . voucher_detail_no , " item_code " : sle . item_code } ,
" incoming_rate " , outgoing_rate )
def update_rate_on_purchase_receipt ( self , sle , outgoing_rate ) :
if frappe . db . exists ( sle . voucher_type + " Item " , sle . voucher_detail_no ) :
frappe . db . set_value ( sle . voucher_type + " Item " , sle . voucher_detail_no , " base_net_rate " , outgoing_rate )
else :
frappe . db . set_value ( " Purchase Receipt Item Supplied " , sle . voucher_detail_no , " rate " , outgoing_rate )
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
2021-06-15 04:51:44 +00:00
if frappe . get_cached_value ( sle . voucher_type , sle . voucher_no , " is_subcontracted " ) == ' Yes ' :
2021-06-18 15:07:42 +00:00
doc = frappe . get_doc ( sle . voucher_type , sle . voucher_no )
2020-12-21 09:15:50 +00:00
doc . update_valuation_rate ( reset_outgoing_rate = False )
for d in ( doc . items + doc . supplied_items ) :
d . db_update ( )
2015-02-17 14:25:17 +00:00
def get_serialized_values ( self , sle ) :
incoming_rate = flt ( sle . incoming_rate )
actual_qty = flt ( sle . actual_qty )
2020-01-02 13:30:32 +00:00
serial_nos = cstr ( sle . serial_no ) . split ( " \n " )
2015-02-17 14:25:17 +00:00
if incoming_rate < 0 :
# wrong incoming rate
2020-12-21 09:15:50 +00:00
incoming_rate = self . wh_data . valuation_rate
2016-06-12 05:33:00 +00:00
2016-02-29 06:00:27 +00:00
stock_value_change = 0
2021-12-21 11:19:41 +00:00
if actual_qty > 0 :
2016-02-29 06:00:27 +00:00
stock_value_change = actual_qty * incoming_rate
2021-12-21 11:19:41 +00:00
else :
2016-02-29 06:00:27 +00:00
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
2020-12-21 09:15:50 +00:00
if not sle . is_cancelled :
outgoing_value = self . get_incoming_value_for_serial_nos ( sle , serial_nos )
stock_value_change = - 1 * outgoing_value
else :
stock_value_change = actual_qty * sle . outgoing_rate
2016-02-29 06:00:27 +00:00
2020-12-21 09:15:50 +00:00
new_stock_qty = self . wh_data . qty_after_transaction + actual_qty
2018-07-27 05:03:30 +00:00
2016-02-29 06:00:27 +00:00
if new_stock_qty > 0 :
2020-12-21 09:15:50 +00:00
new_stock_value = ( self . wh_data . qty_after_transaction * self . wh_data . valuation_rate ) + stock_value_change
2018-07-27 05:03:30 +00:00
if new_stock_value > = 0 :
2016-02-29 06:00:27 +00:00
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
2020-12-21 09:15:50 +00:00
self . wh_data . valuation_rate = new_stock_value / new_stock_qty
2016-07-08 12:54:46 +00:00
2020-12-21 09:15:50 +00:00
if not self . wh_data . valuation_rate and sle . voucher_detail_no :
2017-12-01 10:39:02 +00:00
allow_zero_rate = self . check_if_allow_zero_valuation_rate ( sle . voucher_type , sle . voucher_detail_no )
if not allow_zero_rate :
2022-02-19 14:05:33 +00:00
self . wh_data . valuation_rate = self . get_fallback_rate ( sle )
2017-12-01 10:39:02 +00:00
2020-01-02 13:30:32 +00:00
def get_incoming_value_for_serial_nos ( self , sle , serial_nos ) :
# get rate from serial nos within same company
all_serial_nos = frappe . get_all ( " Serial No " ,
fields = [ " purchase_rate " , " name " , " company " ] ,
filters = { ' name ' : ( ' in ' , serial_nos ) } )
2021-06-11 13:10:22 +00:00
incoming_values = sum ( flt ( d . purchase_rate ) for d in all_serial_nos if d . company == sle . company )
2020-01-02 13:30:32 +00:00
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [ d . name for d in all_serial_nos if d . company != sle . company ]
for serial_no in invalid_serial_nos :
incoming_rate = frappe . db . sql ( """
select incoming_rate
from ` tabStock Ledger Entry `
where
company = % s
and actual_qty > 0
2022-01-16 14:49:04 +00:00
and is_cancelled = 0
2020-01-02 13:30:32 +00:00
and ( serial_no = % s
or serial_no like % s
or serial_no like % s
or serial_no like % s
)
order by posting_date desc
limit 1
""" , (sle.company, serial_no, serial_no+ ' \n % ' , ' % \n ' +serial_no, ' % \n ' +serial_no+ ' \n % ' ))
incoming_values + = flt ( incoming_rate [ 0 ] [ 0 ] ) if incoming_rate else 0
return incoming_values
2015-02-17 14:25:17 +00:00
def get_moving_average_values ( self , sle ) :
actual_qty = flt ( sle . actual_qty )
2020-12-21 09:15:50 +00:00
new_stock_qty = flt ( self . wh_data . qty_after_transaction ) + actual_qty
2016-06-24 06:58:55 +00:00
if new_stock_qty > = 0 :
if actual_qty > 0 :
2020-12-21 09:15:50 +00:00
if flt ( self . wh_data . qty_after_transaction ) < = 0 :
self . wh_data . valuation_rate = sle . incoming_rate
2016-06-24 06:58:55 +00:00
else :
2020-12-21 09:15:50 +00:00
new_stock_value = ( self . wh_data . qty_after_transaction * self . wh_data . valuation_rate ) + \
2016-06-24 06:58:55 +00:00
( actual_qty * sle . incoming_rate )
2015-10-15 06:58:20 +00:00
2020-12-21 09:15:50 +00:00
self . wh_data . valuation_rate = new_stock_value / new_stock_qty
2015-02-17 14:25:17 +00:00
2016-06-24 06:58:55 +00:00
elif sle . outgoing_rate :
if new_stock_qty :
2020-12-21 09:15:50 +00:00
new_stock_value = ( self . wh_data . qty_after_transaction * self . wh_data . valuation_rate ) + \
2016-06-24 06:58:55 +00:00
( actual_qty * sle . outgoing_rate )
2015-02-17 14:25:17 +00:00
2020-12-21 09:15:50 +00:00
self . wh_data . valuation_rate = new_stock_value / new_stock_qty
2016-06-24 06:58:55 +00:00
else :
2020-12-21 09:15:50 +00:00
self . wh_data . valuation_rate = sle . outgoing_rate
2016-06-24 06:58:55 +00:00
else :
2020-12-21 09:15:50 +00:00
if flt ( self . wh_data . qty_after_transaction ) > = 0 and sle . outgoing_rate :
self . wh_data . valuation_rate = sle . outgoing_rate
2015-02-17 14:25:17 +00:00
2020-12-21 09:15:50 +00:00
if not self . wh_data . valuation_rate and actual_qty > 0 :
self . wh_data . valuation_rate = sle . incoming_rate
2017-03-31 07:14:29 +00:00
2017-05-04 04:05:19 +00:00
# Get valuation rate from previous SLE or Item master, if item does not have the
2017-04-14 10:24:04 +00:00
# allow zero valuration rate flag set
2020-12-21 09:15:50 +00:00
if not self . wh_data . valuation_rate and sle . voucher_detail_no :
2017-04-14 10:24:04 +00:00
allow_zero_valuation_rate = self . check_if_allow_zero_valuation_rate ( sle . voucher_type , sle . voucher_detail_no )
if not allow_zero_valuation_rate :
2022-02-19 14:05:33 +00:00
self . wh_data . valuation_rate = self . get_fallback_rate ( sle )
2017-03-31 07:14:29 +00:00
2022-02-02 07:21:21 +00:00
def update_queue_values ( self , sle ) :
2015-02-17 14:25:17 +00:00
incoming_rate = flt ( sle . incoming_rate )
actual_qty = flt ( sle . actual_qty )
2015-07-17 09:39:56 +00:00
outgoing_rate = flt ( sle . outgoing_rate )
2015-02-17 14:25:17 +00:00
2022-02-19 15:28:36 +00:00
self . wh_data . qty_after_transaction = round_off_if_near_zero ( self . wh_data . qty_after_transaction + actual_qty )
2022-01-15 12:12:25 +00:00
if self . valuation_method == " LIFO " :
stock_queue = LIFOValuation ( self . wh_data . stock_queue )
else :
stock_queue = FIFOValuation ( self . wh_data . stock_queue )
2022-02-19 15:28:36 +00:00
_prev_qty , prev_stock_value = stock_queue . get_total_stock_and_value ( )
2015-02-17 14:25:17 +00:00
if actual_qty > 0 :
2022-01-15 12:12:25 +00:00
stock_queue . add_stock ( qty = actual_qty , rate = incoming_rate )
2015-02-17 14:25:17 +00:00
else :
2021-12-18 13:10:22 +00:00
def rate_generator ( ) - > float :
allow_zero_valuation_rate = self . check_if_allow_zero_valuation_rate ( sle . voucher_type , sle . voucher_detail_no )
if not allow_zero_valuation_rate :
2022-02-19 14:05:33 +00:00
return self . get_fallback_rate ( sle )
2015-07-17 09:39:56 +00:00
else :
2021-12-18 13:10:22 +00:00
return 0.0
2015-02-17 14:25:17 +00:00
2022-01-15 12:12:25 +00:00
stock_queue . remove_stock ( qty = abs ( actual_qty ) , outgoing_rate = outgoing_rate , rate_generator = rate_generator )
2015-02-17 14:25:17 +00:00
2022-02-19 15:28:36 +00:00
_qty , stock_value = stock_queue . get_total_stock_and_value ( )
2015-02-17 14:25:17 +00:00
2022-02-19 15:28:36 +00:00
stock_value_difference = stock_value - prev_stock_value
2021-12-18 13:10:22 +00:00
2022-02-19 15:28:36 +00:00
self . wh_data . stock_queue = stock_queue . state
self . wh_data . stock_value = round_off_if_near_zero ( self . wh_data . stock_value + stock_value_difference )
2016-07-08 12:54:46 +00:00
2020-12-21 09:15:50 +00:00
if not self . wh_data . stock_queue :
self . wh_data . stock_queue . append ( [ 0 , sle . incoming_rate or sle . outgoing_rate or self . wh_data . valuation_rate ] )
2017-03-31 07:14:29 +00:00
2022-02-19 15:28:36 +00:00
if self . wh_data . qty_after_transaction :
self . wh_data . valuation_rate = self . wh_data . stock_value / self . wh_data . qty_after_transaction
2022-02-15 06:11:41 +00:00
def update_batched_values ( self , sle ) :
incoming_rate = flt ( sle . incoming_rate )
actual_qty = flt ( sle . actual_qty )
2022-02-19 16:52:27 +00:00
self . wh_data . qty_after_transaction = round_off_if_near_zero ( self . wh_data . qty_after_transaction + actual_qty )
2022-02-15 06:11:41 +00:00
if actual_qty > 0 :
stock_value_difference = incoming_rate * actual_qty
else :
2022-02-19 16:52:27 +00:00
outgoing_rate = get_batch_incoming_rate ( item_code = sle . item_code ,
warehouse = sle . warehouse , batch_no = sle . batch_no , posting_date = sle . posting_date ,
posting_time = sle . posting_time , creation = sle . creation )
2022-02-19 14:06:28 +00:00
if outgoing_rate is None :
# This can *only* happen if qty available for the batch is zero.
# in such case fall back various other rates.
# future entries will correct the overall accounting as each
# batch individually uses moving average rates.
outgoing_rate = self . get_fallback_rate ( sle )
2022-02-15 06:11:41 +00:00
stock_value_difference = outgoing_rate * actual_qty
2021-12-18 13:10:22 +00:00
2022-02-19 16:52:27 +00:00
self . wh_data . stock_value = round_off_if_near_zero ( self . wh_data . stock_value + stock_value_difference )
2022-02-15 06:11:41 +00:00
if self . wh_data . qty_after_transaction :
self . wh_data . valuation_rate = self . wh_data . stock_value / self . wh_data . qty_after_transaction
2021-12-18 13:10:22 +00:00
2017-04-14 10:24:04 +00:00
def check_if_allow_zero_valuation_rate ( self , voucher_type , voucher_detail_no ) :
2019-07-30 13:19:19 +00:00
ref_item_dt = " "
if voucher_type == " Stock Entry " :
ref_item_dt = voucher_type + " Detail "
elif voucher_type in [ " Purchase Invoice " , " Sales Invoice " , " Delivery Note " , " Purchase Receipt " ] :
ref_item_dt = voucher_type + " Item "
if ref_item_dt :
return frappe . db . get_value ( ref_item_dt , voucher_detail_no , " allow_zero_valuation_rate " )
else :
return 0
2017-03-31 07:14:29 +00:00
2022-02-19 14:05:33 +00:00
def get_fallback_rate ( self , sle ) - > float :
""" When exact incoming rate isn ' t available use any of other " average " rates as fallback.
This should only get used for negative stock . """
return get_valuation_rate ( sle . item_code , sle . warehouse ,
sle . voucher_type , sle . voucher_no , self . allow_zero_rate ,
currency = erpnext . get_company_currency ( sle . company ) , company = sle . company , batch_no = sle . batch_no )
2020-12-21 09:15:50 +00:00
def get_sle_before_datetime ( self , args ) :
2015-02-17 14:25:17 +00:00
""" get previous stock ledger entry before current time-bucket """
2020-12-21 09:15:50 +00:00
sle = get_stock_ledger_entries ( args , " < " , " desc " , " limit 1 " , for_update = False )
sle = sle [ 0 ] if sle else frappe . _dict ( )
return sle
2015-02-17 14:25:17 +00:00
2020-12-21 09:15:50 +00:00
def get_sle_after_datetime ( self , args ) :
2015-02-17 14:25:17 +00:00
""" get Stock Ledger Entries after a particular datetime, for reposting """
2020-12-21 09:15:50 +00:00
return get_stock_ledger_entries ( args , " > " , " asc " , for_update = True , check_serial_no = False )
2015-02-17 14:25:17 +00:00
def raise_exceptions ( self ) :
2020-12-21 09:15:50 +00:00
msg_list = [ ]
for warehouse , exceptions in self . exceptions . items ( ) :
deficiency = min ( e [ " diff " ] for e in exceptions )
2016-06-12 05:33:00 +00:00
2020-12-21 09:15:50 +00:00
if ( ( exceptions [ 0 ] [ " voucher_type " ] , exceptions [ 0 ] [ " voucher_no " ] ) in
frappe . local . flags . currently_saving ) :
2016-07-20 10:43:18 +00:00
2020-12-21 09:15:50 +00:00
msg = _ ( " {0} units of {1} needed in {2} to complete this transaction. " ) . format (
2021-02-02 11:25:13 +00:00
abs ( deficiency ) , frappe . get_desk_link ( ' Item ' , exceptions [ 0 ] [ " item_code " ] ) ,
2020-12-21 09:15:50 +00:00
frappe . get_desk_link ( ' Warehouse ' , warehouse ) )
else :
msg = _ ( " {0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction. " ) . format (
2021-02-02 11:25:13 +00:00
abs ( deficiency ) , frappe . get_desk_link ( ' Item ' , exceptions [ 0 ] [ " item_code " ] ) ,
2020-12-21 09:15:50 +00:00
frappe . get_desk_link ( ' Warehouse ' , warehouse ) ,
exceptions [ 0 ] [ " posting_date " ] , exceptions [ 0 ] [ " posting_time " ] ,
frappe . get_desk_link ( exceptions [ 0 ] [ " voucher_type " ] , exceptions [ 0 ] [ " voucher_no " ] ) )
if msg :
msg_list . append ( msg )
if msg_list :
message = " \n \n " . join ( msg_list )
if self . verbose :
frappe . throw ( message , NegativeStockError , title = ' Insufficient Stock ' )
else :
raise NegativeStockError ( message )
2021-01-28 07:39:56 +00:00
2020-12-21 09:15:50 +00:00
def update_bin ( self ) :
# update bin for each warehouse
for warehouse , data in self . data . items ( ) :
2021-12-03 06:20:38 +00:00
bin_name = get_or_make_bin ( self . item_code , warehouse )
2021-10-12 14:45:55 +00:00
2021-12-03 06:20:38 +00:00
frappe . db . set_value ( ' Bin ' , bin_name , {
2020-12-21 09:15:50 +00:00
" valuation_rate " : data . valuation_rate ,
" actual_qty " : data . qty_after_transaction ,
" stock_value " : data . stock_value
} )
2014-04-07 06:32:57 +00:00
2021-06-22 16:05:25 +00:00
def get_previous_sle_of_current_voucher ( args , exclude_current_voucher = False ) :
""" get stock ledger entries filtered by specific posting datetime conditions """
args [ ' time_format ' ] = ' % H: %i : %s '
if not args . get ( " posting_date " ) :
args [ " posting_date " ] = " 1900-01-01 "
if not args . get ( " posting_time " ) :
args [ " posting_time " ] = " 00:00 "
voucher_condition = " "
if exclude_current_voucher :
voucher_no = args . get ( " voucher_no " )
voucher_condition = f " and voucher_no != ' { voucher_no } ' "
sle = 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 is_cancelled = 0
{ voucher_condition }
and timestamp ( posting_date , time_format ( posting_time , % ( time_format ) s ) ) < timestamp ( % ( posting_date ) s , time_format ( % ( posting_time ) s , % ( time_format ) s ) )
order by timestamp ( posting_date , posting_time ) desc , creation desc
limit 1
for update """ .format(voucher_condition=voucher_condition), args, as_dict=1)
return sle [ 0 ] if sle else frappe . _dict ( )
2013-01-11 06:14:49 +00:00
def get_previous_sle ( args , for_update = False ) :
2013-01-10 13:59:51 +00:00
"""
2014-04-07 06:32:57 +00:00
get the last sle on or before the current time - bucket ,
2013-01-10 13:59:51 +00:00
to get actual qty before transaction , this function
is called from various transaction like stock entry , reco etc
2014-04-07 06:32:57 +00:00
2013-01-10 13:59:51 +00:00
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 14:25:17 +00:00
args [ " name " ] = args . get ( " sle " , None ) or " "
sle = get_stock_ledger_entries ( args , " <= " , " desc " , " limit 1 " , for_update = for_update )
2013-09-18 13:01:03 +00:00
return sle and sle [ 0 ] or { }
2014-10-15 06:04:40 +00:00
2019-05-24 11:23:51 +00:00
def get_stock_ledger_entries ( previous_sle , operator = None ,
order = " desc " , limit = None , for_update = False , debug = False , check_serial_no = True ) :
2015-02-17 14:25:17 +00:00
""" get stock ledger entries filtered by specific posting datetime conditions """
2018-02-01 09:28:50 +00:00
conditions = " and timestamp(posting_date, posting_time) {0} timestamp( %(posting_date)s , %(posting_time)s ) " . format ( operator )
if previous_sle . get ( " warehouse " ) :
conditions + = " and warehouse = %(warehouse)s "
elif previous_sle . get ( " warehouse_condition " ) :
conditions + = " and " + previous_sle . get ( " warehouse_condition " )
2019-05-24 11:23:51 +00:00
if check_serial_no and previous_sle . get ( " serial_no " ) :
2021-04-24 11:58:33 +00:00
# conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
serial_no = previous_sle . get ( " serial_no " )
conditions + = ( """ and
(
serial_no = { 0 }
or serial_no like { 1 }
or serial_no like { 2 }
or serial_no like { 3 }
)
""" ).format(frappe.db.escape(serial_no), frappe.db.escape( ' {} \n % ' .format(serial_no)),
frappe . db . escape ( ' % \n {} ' . format ( serial_no ) ) , frappe . db . escape ( ' % \n {} \n % ' . format ( serial_no ) ) )
2019-04-28 13:09:18 +00:00
2015-02-17 14:25:17 +00:00
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 "
2020-04-30 05:08:58 +00:00
return frappe . db . sql ( """
select * , timestamp ( posting_date , posting_time ) as " timestamp "
from ` tabStock Ledger Entry `
2015-02-17 14:25:17 +00:00
where item_code = % % ( item_code ) s
2020-12-21 09:15:50 +00:00
and is_cancelled = 0
2018-02-01 09:28:50 +00:00
% ( conditions ) s
2019-01-07 16:37:13 +00:00
order by timestamp ( posting_date , posting_time ) % ( order ) s , creation % ( order ) s
2015-02-17 14:25:17 +00:00
% ( limit ) s % ( for_update ) s """ % {
" conditions " : conditions ,
" limit " : limit or " " ,
" for_update " : for_update and " for update " or " " ,
" order " : order
2015-02-19 14:35:45 +00:00
} , previous_sle , as_dict = 1 , debug = debug )
2015-02-17 14:25:17 +00:00
2020-12-21 09:15:50 +00:00
def get_sle_by_voucher_detail_no ( voucher_detail_no , excluded_sle = None ) :
return frappe . db . get_value ( ' Stock Ledger Entry ' ,
{ ' voucher_detail_no ' : voucher_detail_no , ' name ' : [ ' != ' , excluded_sle ] } ,
[ ' item_code ' , ' warehouse ' , ' posting_date ' , ' posting_time ' , ' timestamp(posting_date, posting_time) as timestamp ' ] ,
as_dict = 1 )
2020-04-30 05:08:58 +00:00
2022-02-19 10:07:03 +00:00
def get_batch_incoming_rate ( item_code , warehouse , batch_no , posting_date , posting_time , creation = None ) :
2022-02-15 06:11:41 +00:00
2022-02-19 10:21:04 +00:00
Timestamp = CustomFunction ( ' timestamp ' , [ ' date ' , ' time ' ] )
sle = frappe . qb . DocType ( " Stock Ledger Entry " )
timestamp_condition = ( Timestamp ( sle . posting_date , sle . posting_time ) < Timestamp ( posting_date , posting_time ) )
if creation :
timestamp_condition | = (
( Timestamp ( sle . posting_date , sle . posting_time ) == Timestamp ( posting_date , posting_time ) )
& ( sle . creation < creation )
2022-02-15 06:11:41 +00:00
)
2022-02-19 10:21:04 +00:00
batch_details = (
frappe . qb
. from_ ( sle )
. select (
Sum ( sle . stock_value_difference ) . as_ ( " batch_value " ) ,
Sum ( sle . actual_qty ) . as_ ( " batch_qty " )
)
. where (
( sle . item_code == item_code )
& ( sle . warehouse == warehouse )
& ( sle . batch_no == batch_no )
& ( sle . is_cancelled == 0 )
)
. where ( timestamp_condition )
) . run ( as_dict = True )
2022-02-15 06:11:41 +00:00
if batch_details and batch_details [ 0 ] . batch_qty :
return batch_details [ 0 ] . batch_value / batch_details [ 0 ] . batch_qty
2017-03-31 07:14:29 +00:00
def get_valuation_rate ( item_code , warehouse , voucher_type , voucher_no ,
2022-02-19 08:58:51 +00:00
allow_zero_rate = False , currency = None , company = None , raise_error_if_no_rate = True , batch_no = None ) :
2021-11-01 07:51:14 +00:00
2017-06-16 09:51:36 +00:00
if not company :
2021-11-01 07:51:14 +00:00
company = frappe . get_cached_value ( " Warehouse " , warehouse , " company " )
2017-06-16 09:51:36 +00:00
2022-02-19 08:58:51 +00:00
last_valuation_rate = None
# Get moving average rate of a specific batch number
if warehouse and batch_no and frappe . db . get_value ( " Batch " , batch_no , " use_batchwise_valuation " ) :
last_valuation_rate = frappe . db . sql ( """
select sum ( stock_value_difference ) / sum ( actual_qty )
from ` tabStock Ledger Entry `
where
item_code = % s
AND warehouse = % s
AND batch_no = % s
AND is_cancelled = 0
AND NOT ( voucher_no = % s AND voucher_type = % s )
""" ,
( item_code , warehouse , batch_no , voucher_no , voucher_type ) )
2021-11-01 07:51:14 +00:00
# Get valuation rate from last sle for the same item and warehouse
2022-02-19 08:58:51 +00:00
if not last_valuation_rate or last_valuation_rate [ 0 ] [ 0 ] is None :
last_valuation_rate = frappe . db . sql ( """ select valuation_rate
from ` tabStock Ledger Entry ` force index ( item_warehouse )
where
item_code = % s
AND warehouse = % s
AND valuation_rate > = 0
AND is_cancelled = 0
AND NOT ( voucher_no = % s AND voucher_type = % s )
order by posting_date desc , posting_time desc , name desc limit 1 """ , (item_code, warehouse, voucher_no, voucher_type))
2014-10-15 06:04:40 +00:00
if not last_valuation_rate :
2017-01-18 13:05:58 +00:00
# Get valuation rate from last sle for the item against any warehouse
2014-10-15 06:04:40 +00:00
last_valuation_rate = frappe . db . sql ( """ select valuation_rate
2021-10-12 14:45:55 +00:00
from ` tabStock Ledger Entry ` force index ( item_code )
2019-08-19 04:34:52 +00:00
where
item_code = % s
AND valuation_rate > 0
2022-01-16 14:49:04 +00:00
AND is_cancelled = 0
2019-08-19 04:34:52 +00:00
AND NOT ( voucher_no = % s AND voucher_type = % s )
order by posting_date desc , posting_time desc , name desc limit 1 """ , (item_code, voucher_no, voucher_type))
2014-10-15 06:04:40 +00:00
2018-03-01 05:01:24 +00:00
if last_valuation_rate :
2020-12-21 09:15:50 +00:00
return flt ( last_valuation_rate [ 0 ] [ 0 ] )
2018-03-01 05:01:24 +00:00
# If negative stock allowed, and item delivered without any incoming entry,
# system does not found any SLE, then take valuation rate from Item
valuation_rate = frappe . db . get_value ( " Item " , item_code , " valuation_rate " )
2014-10-15 06:04:40 +00:00
if not valuation_rate :
2018-03-01 05:01:24 +00:00
# try Item Standard rate
valuation_rate = frappe . db . get_value ( " Item " , item_code , " standard_rate " )
2017-01-18 13:05:58 +00:00
2017-05-04 04:05:19 +00:00
if not valuation_rate :
2018-03-01 05:01:24 +00:00
# try in price list
valuation_rate = frappe . db . get_value ( ' Item Price ' ,
dict ( item_code = item_code , buying = 1 , currency = currency ) ,
' price_list_rate ' )
2017-03-31 07:14:29 +00:00
2018-02-01 05:21:27 +00:00
if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
2017-06-19 07:24:59 +00:00
and cint ( erpnext . is_perpetual_inventory_enabled ( company ) ) :
2017-03-28 12:09:34 +00:00
frappe . local . message_log = [ ]
2021-04-24 11:58:33 +00:00
form_link = get_link_to_form ( " Item " , item_code )
2020-05-11 15:15:37 +00:00
message = _ ( " Valuation Rate for the Item {0} , is required to do accounting entries for {1} {2} . " ) . format ( form_link , voucher_type , voucher_no )
2021-04-24 11:58:33 +00:00
message + = " <br><br> " + _ ( " Here are the options to proceed: " )
2020-05-11 15:15:37 +00:00
solutions = " <li> " + _ ( " If the item is transacting as a Zero Valuation Rate item in this entry, please enable ' Allow Zero Valuation Rate ' in the {0} Item table. " ) . format ( voucher_type ) + " </li> "
2021-04-24 11:58:33 +00:00
solutions + = " <li> " + _ ( " If not, you can Cancel / Submit this entry " ) + " {0} " . format ( frappe . bold ( " after " ) ) + _ ( " performing either one below: " ) + " </li> "
2020-05-11 15:15:37 +00:00
sub_solutions = " <ul><li> " + _ ( " Create an incoming stock transaction for the Item. " ) + " </li> "
sub_solutions + = " <li> " + _ ( " Mention Valuation Rate in the Item master. " ) + " </li></ul> "
msg = message + solutions + sub_solutions + " </li> "
frappe . throw ( msg = msg , title = _ ( " Valuation Rate Missing " ) )
2014-10-15 06:04:40 +00:00
return valuation_rate
2020-12-21 09:15:50 +00:00
2021-08-26 11:10:45 +00:00
def update_qty_in_future_sle ( args , allow_negative_stock = False ) :
2021-06-22 16:05:25 +00:00
""" Recalculate Qty after Transaction in future SLEs based on current SLE. """
2021-07-02 11:43:45 +00:00
datetime_limit_condition = " "
2021-06-22 16:05:25 +00:00
qty_shift = args . actual_qty
# find difference/shift in qty caused by stock reconciliation
if args . voucher_type == " Stock Reconciliation " :
2021-07-02 11:43:45 +00:00
qty_shift = get_stock_reco_qty_shift ( args )
# find the next nearest stock reco so that we only recalculate SLEs till that point
next_stock_reco_detail = get_next_stock_reco ( args )
if next_stock_reco_detail :
detail = next_stock_reco_detail [ 0 ]
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition ( detail )
2021-06-22 16:05:25 +00:00
2021-02-18 08:44:21 +00:00
frappe . db . sql ( """
update ` tabStock Ledger Entry `
2021-06-22 16:05:25 +00:00
set qty_after_transaction = qty_after_transaction + { qty_shift }
2021-02-18 08:44:21 +00:00
where
item_code = % ( item_code ) s
and warehouse = % ( warehouse ) s
and voucher_no != % ( voucher_no ) s
and is_cancelled = 0
and ( timestamp ( posting_date , posting_time ) > timestamp ( % ( posting_date ) s , % ( posting_time ) s )
or (
timestamp ( posting_date , posting_time ) = timestamp ( % ( posting_date ) s , % ( posting_time ) s )
and creation > % ( creation ) s
)
)
2021-07-02 11:43:45 +00:00
{ datetime_limit_condition }
""" .format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
2021-02-18 08:44:21 +00:00
validate_negative_qty_in_future_sle ( args , allow_negative_stock )
2021-07-02 11:43:45 +00:00
def get_stock_reco_qty_shift ( args ) :
stock_reco_qty_shift = 0
if args . get ( " is_cancelled " ) :
if args . get ( " previous_qty_after_transaction " ) :
# get qty (balance) that was set at submission
last_balance = args . get ( " previous_qty_after_transaction " )
stock_reco_qty_shift = flt ( args . qty_after_transaction ) - flt ( last_balance )
else :
stock_reco_qty_shift = flt ( args . actual_qty )
else :
# reco is being submitted
last_balance = get_previous_sle_of_current_voucher ( args ,
exclude_current_voucher = True ) . get ( " qty_after_transaction " )
if last_balance is not None :
stock_reco_qty_shift = flt ( args . qty_after_transaction ) - flt ( last_balance )
else :
stock_reco_qty_shift = args . qty_after_transaction
return stock_reco_qty_shift
def get_next_stock_reco ( args ) :
""" Returns next nearest stock reconciliaton ' s details. """
return frappe . db . sql ( """
select
name , posting_date , posting_time , creation , voucher_no
from
2021-07-02 12:16:05 +00:00
` tabStock Ledger Entry `
2021-07-02 11:43:45 +00:00
where
item_code = % ( item_code ) s
and warehouse = % ( warehouse ) s
and voucher_type = ' Stock Reconciliation '
and voucher_no != % ( voucher_no ) s
and is_cancelled = 0
and ( timestamp ( posting_date , posting_time ) > timestamp ( % ( posting_date ) s , % ( posting_time ) s )
or (
timestamp ( posting_date , posting_time ) = timestamp ( % ( posting_date ) s , % ( posting_time ) s )
and creation > % ( creation ) s
)
)
limit 1
""" , args, as_dict=1)
def get_datetime_limit_condition ( detail ) :
return f """
and
( timestamp ( posting_date , posting_time ) < timestamp ( ' {detail.posting_date} ' , ' {detail.posting_time} ' )
or (
timestamp ( posting_date , posting_time ) = timestamp ( ' {detail.posting_date} ' , ' {detail.posting_time} ' )
and creation < ' {detail.creation} '
)
) """
2021-08-26 11:10:45 +00:00
def validate_negative_qty_in_future_sle ( args , allow_negative_stock = False ) :
2022-02-12 07:38:28 +00:00
if allow_negative_stock or is_negative_stock_allowed ( item_code = args . item_code ) :
2021-12-07 17:33:52 +00:00
return
if not ( args . actual_qty < 0 or args . voucher_type == " Stock Reconciliation " ) :
return
neg_sle = get_future_sle_with_negative_qty ( args )
if neg_sle :
message = _ ( " {0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction. " ) . format (
abs ( neg_sle [ 0 ] [ " qty_after_transaction " ] ) ,
frappe . get_desk_link ( ' Item ' , args . item_code ) ,
frappe . get_desk_link ( ' Warehouse ' , args . warehouse ) ,
neg_sle [ 0 ] [ " posting_date " ] , neg_sle [ 0 ] [ " posting_time " ] ,
frappe . get_desk_link ( neg_sle [ 0 ] [ " voucher_type " ] , neg_sle [ 0 ] [ " voucher_no " ] ) )
frappe . throw ( message , NegativeStockError , title = ' Insufficient Stock ' )
if not args . batch_no :
return
neg_batch_sle = get_future_sle_with_negative_batch_qty ( args )
if neg_batch_sle :
message = _ ( " {0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction. " ) . format (
abs ( neg_batch_sle [ 0 ] [ " cumulative_total " ] ) ,
frappe . get_desk_link ( ' Batch ' , args . batch_no ) ,
frappe . get_desk_link ( ' Warehouse ' , args . warehouse ) ,
neg_batch_sle [ 0 ] [ " posting_date " ] , neg_batch_sle [ 0 ] [ " posting_time " ] ,
frappe . get_desk_link ( neg_batch_sle [ 0 ] [ " voucher_type " ] , neg_batch_sle [ 0 ] [ " voucher_no " ] ) )
frappe . throw ( message , NegativeStockError , title = " Insufficient Stock for Batch " )
2020-12-21 09:15:50 +00:00
def get_future_sle_with_negative_qty ( args ) :
return frappe . db . sql ( """
select
qty_after_transaction , posting_date , posting_time ,
voucher_type , voucher_no
from ` tabStock Ledger Entry `
2021-01-28 07:39:56 +00:00
where
2020-12-21 09:15:50 +00:00
item_code = % ( item_code ) s
and warehouse = % ( warehouse ) s
and voucher_no != % ( voucher_no ) s
and timestamp ( posting_date , posting_time ) > = timestamp ( % ( posting_date ) s , % ( posting_time ) s )
and is_cancelled = 0
2021-02-18 08:44:21 +00:00
and qty_after_transaction < 0
2021-02-02 11:25:13 +00:00
order by timestamp ( posting_date , posting_time ) asc
2020-12-21 09:15:50 +00:00
limit 1
2021-03-31 07:14:03 +00:00
""" , args, as_dict=1)
2021-04-12 14:51:27 +00:00
2021-12-07 17:33:52 +00:00
def get_future_sle_with_negative_batch_qty ( args ) :
return frappe . db . sql ( """
with batch_ledger as (
select
posting_date , posting_time , voucher_type , voucher_no ,
sum ( actual_qty ) over ( order by posting_date , posting_time , creation ) as cumulative_total
from ` tabStock Ledger Entry `
where
item_code = % ( item_code ) s
and warehouse = % ( warehouse ) s
and batch_no = % ( batch_no ) s
and is_cancelled = 0
order by posting_date , posting_time , creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp ( posting_date , posting_time ) > = timestamp ( % ( posting_date ) s , % ( posting_time ) s )
limit 1
""" , args, as_dict=1)
2022-02-12 07:38:28 +00:00
def is_negative_stock_allowed ( * , item_code : Optional [ str ] = None ) - > bool :
if cint ( frappe . db . get_single_value ( " Stock Settings " , " allow_negative_stock " , cache = True ) ) :
return True
if item_code and cint ( frappe . db . get_value ( " Item " , item_code , " allow_negative_stock " , cache = True ) ) :
return True
return False