2023-03-13 09:21:43 +00:00
from collections import defaultdict
from typing import List
2023-03-06 06:38:28 +00:00
import frappe
2023-03-13 09:21:43 +00:00
from frappe import _ , bold
2023-03-06 06:38:28 +00:00
from frappe . model . naming import make_autoname
2023-03-20 08:45:34 +00:00
from frappe . query_builder . functions import CombineDatetime , Sum
2023-06-14 17:52:22 +00:00
from frappe . utils import cint , flt , get_link_to_form , now , nowtime , today
2023-03-13 09:21:43 +00:00
from erpnext . stock . deprecated_serial_batch import (
DeprecatedBatchNoValuation ,
DeprecatedSerialNoValuation ,
)
2023-03-06 06:38:28 +00:00
from erpnext . stock . valuation import round_off_if_near_zero
class SerialBatchBundle :
def __init__ ( self , * * kwargs ) :
2023-03-13 09:21:43 +00:00
for key , value in kwargs . items ( ) :
2023-03-06 06:38:28 +00:00
setattr ( self , key , value )
self . set_item_details ( )
2023-03-13 09:21:43 +00:00
self . process_serial_and_batch_bundle ( )
if self . sle . is_cancelled :
self . delink_serial_and_batch_bundle ( )
self . post_process ( )
2023-03-06 06:38:28 +00:00
def process_serial_and_batch_bundle ( self ) :
if self . item_details . has_serial_no :
2023-03-13 09:21:43 +00:00
self . process_serial_no ( )
2023-03-06 06:38:28 +00:00
elif self . item_details . has_batch_no :
2023-03-13 09:21:43 +00:00
self . process_batch_no ( )
2023-03-06 06:38:28 +00:00
def set_item_details ( self ) :
fields = [
" has_batch_no " ,
" has_serial_no " ,
" item_name " ,
" item_group " ,
" serial_no_series " ,
" create_new_batch " ,
" batch_number_series " ,
]
self . item_details = frappe . get_cached_value ( " Item " , self . sle . item_code , fields , as_dict = 1 )
def process_serial_no ( self ) :
if (
not self . sle . is_cancelled
and not self . sle . serial_and_batch_bundle
and self . item_details . has_serial_no == 1
) :
2023-03-13 09:21:43 +00:00
self . make_serial_batch_no_bundle ( )
elif not self . sle . is_cancelled :
self . validate_item_and_warehouse ( )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
def make_serial_batch_no_bundle ( self ) :
2023-03-28 06:46:27 +00:00
self . validate_item ( )
2023-03-13 09:21:43 +00:00
2023-03-28 06:46:27 +00:00
sn_doc = SerialBatchCreation (
{
" item_code " : self . item_code ,
" warehouse " : self . warehouse ,
" posting_date " : self . sle . posting_date ,
" posting_time " : self . sle . posting_time ,
" voucher_type " : self . sle . voucher_type ,
" voucher_no " : self . sle . voucher_no ,
" voucher_detail_no " : self . sle . voucher_detail_no ,
2023-03-29 06:10:36 +00:00
" qty " : self . sle . actual_qty ,
2023-03-28 06:46:27 +00:00
" avg_rate " : self . sle . incoming_rate ,
" total_amount " : flt ( self . sle . actual_qty ) * flt ( self . sle . incoming_rate ) ,
" type_of_transaction " : " Inward " if self . sle . actual_qty > 0 else " Outward " ,
" company " : self . company ,
" is_rejected " : self . is_rejected_entry ( ) ,
}
) . make_serial_and_batch_bundle ( )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
self . set_serial_and_batch_bundle ( sn_doc )
2023-03-06 06:38:28 +00:00
2023-05-27 13:48:03 +00:00
def validate_actual_qty ( self , sn_doc ) :
2023-06-14 17:52:22 +00:00
link = get_link_to_form ( " Serial and Batch Bundle " , sn_doc . name )
condition = {
" Inward " : self . sle . actual_qty > 0 ,
" Outward " : self . sle . actual_qty < 0 ,
} . get ( sn_doc . type_of_transaction )
if not condition :
correct_type = " Inward "
if sn_doc . type_of_transaction == " Inward " :
correct_type = " Outward "
msg = f " The type of transaction of Serial and Batch Bundle { link } is { bold ( sn_doc . type_of_transaction ) } but as per the Actual Qty { self . sle . actual_qty } for the item { bold ( self . sle . item_code ) } in the { self . sle . voucher_type } { self . sle . voucher_no } the type of transaction should be { bold ( correct_type ) } "
frappe . throw ( _ ( msg ) , title = _ ( " Incorrect Type of Transaction " ) )
2023-05-27 13:48:03 +00:00
precision = sn_doc . precision ( " total_qty " )
if flt ( sn_doc . total_qty , precision ) != flt ( self . sle . actual_qty , precision ) :
2023-06-14 17:52:22 +00:00
msg = f " Total qty { flt ( sn_doc . total_qty , precision ) } of Serial and Batch Bundle { link } is not equal to Actual Qty { flt ( self . sle . actual_qty , precision ) } in the { self . sle . voucher_type } { self . sle . voucher_no } "
2023-05-27 13:48:03 +00:00
frappe . throw ( _ ( msg ) )
2023-03-28 06:46:27 +00:00
def validate_item ( self ) :
msg = " "
if self . sle . actual_qty > 0 :
if not self . item_details . has_batch_no and not self . item_details . has_serial_no :
msg = f " Item { self . item_code } is not a batch or serial no item "
if self . item_details . has_serial_no and not self . item_details . serial_no_series :
msg + = f " . If you want auto pick serial bundle, then kindly set Serial No Series in Item { self . item_code } "
if (
self . item_details . has_batch_no
and not self . item_details . batch_number_series
and not frappe . db . get_single_value ( " Stock Settings " , " naming_series_prefix " )
) :
msg + = f " . If you want auto pick batch bundle, then kindly set Batch Number Series in Item { self . item_code } "
elif self . sle . actual_qty < 0 :
if not frappe . db . get_single_value (
" Stock Settings " , " auto_create_serial_and_batch_bundle_for_outward "
) :
msg + = " . If you want auto pick serial/batch bundle, then kindly enable ' Auto Create Serial and Batch Bundle ' in Stock Settings. "
if msg :
error_msg = (
f " Serial and Batch Bundle not set for item { self . item_code } in warehouse { self . warehouse } . "
+ msg
)
frappe . throw ( _ ( error_msg ) )
2023-03-13 09:21:43 +00:00
def set_serial_and_batch_bundle ( self , sn_doc ) :
self . sle . db_set ( " serial_and_batch_bundle " , sn_doc . name )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
if sn_doc . is_rejected :
frappe . db . set_value (
self . child_doctype , self . sle . voucher_detail_no , " rejected_serial_and_batch_bundle " , sn_doc . name
)
else :
frappe . db . set_value (
self . child_doctype , self . sle . voucher_detail_no , " serial_and_batch_bundle " , sn_doc . name
)
@property
def child_doctype ( self ) :
child_doctype = self . sle . voucher_type + " Item "
if self . sle . voucher_type == " Stock Entry " :
child_doctype = " Stock Entry Detail "
2023-04-05 20:06:18 +00:00
if self . sle . voucher_type == " Asset Capitalization " :
child_doctype = " Asset Capitalization Stock Item "
if self . sle . voucher_type == " Asset Repair " :
child_doctype = " Asset Repair Consumed Item "
2023-03-13 09:21:43 +00:00
return child_doctype
def is_rejected_entry ( self ) :
return is_rejected ( self . sle . voucher_type , self . sle . voucher_detail_no , self . sle . warehouse )
2023-03-06 06:38:28 +00:00
def process_batch_no ( self ) :
if (
not self . sle . is_cancelled
and not self . sle . serial_and_batch_bundle
and self . item_details . has_batch_no == 1
and self . item_details . create_new_batch
2023-03-13 09:21:43 +00:00
) :
self . make_serial_batch_no_bundle ( )
elif not self . sle . is_cancelled :
self . validate_item_and_warehouse ( )
def validate_item_and_warehouse ( self ) :
if self . sle . serial_and_batch_bundle and not frappe . db . exists (
" Serial and Batch Bundle " ,
{
" name " : self . sle . serial_and_batch_bundle ,
" item_code " : self . item_code ,
" warehouse " : self . warehouse ,
" voucher_no " : self . sle . voucher_no ,
} ,
2023-03-06 06:38:28 +00:00
) :
2023-03-13 09:21:43 +00:00
msg = f """
The Serial and Batch Bundle
{ bold ( self . sle . serial_and_batch_bundle ) }
does not belong to Item { bold ( self . item_code ) }
or Warehouse { bold ( self . warehouse ) }
or { self . sle . voucher_type } no { bold ( self . sle . voucher_no ) }
"""
frappe . throw ( _ ( msg ) )
def delink_serial_and_batch_bundle ( self ) :
update_values = {
" serial_and_batch_bundle " : " " ,
}
if is_rejected ( self . sle . voucher_type , self . sle . voucher_detail_no , self . sle . warehouse ) :
update_values [ " rejected_serial_and_batch_bundle " ] = " "
frappe . db . set_value ( self . child_doctype , self . sle . voucher_detail_no , update_values )
frappe . db . set_value (
" Serial and Batch Bundle " ,
2023-03-16 07:28:48 +00:00
{ " voucher_no " : self . sle . voucher_no , " voucher_type " : self . sle . voucher_type } ,
2023-03-13 09:21:43 +00:00
{ " is_cancelled " : 1 , " voucher_no " : " " } ,
)
2023-04-04 06:20:38 +00:00
if self . sle . serial_and_batch_bundle :
frappe . get_cached_doc (
" Serial and Batch Bundle " , self . sle . serial_and_batch_bundle
) . validate_serial_and_batch_inventory ( )
2023-03-13 09:21:43 +00:00
def post_process ( self ) :
2023-03-17 11:12:59 +00:00
if not self . sle . serial_and_batch_bundle :
return
2023-03-31 03:33:54 +00:00
docstatus = frappe . get_cached_value (
" Serial and Batch Bundle " , self . sle . serial_and_batch_bundle , " docstatus "
)
if docstatus != 1 :
self . submit_serial_and_batch_bundle ( )
2023-03-16 07:28:48 +00:00
if self . item_details . has_serial_no == 1 :
self . set_warehouse_and_status_in_serial_nos ( )
2023-03-13 09:21:43 +00:00
2023-03-16 07:28:48 +00:00
if (
self . sle . actual_qty > 0
and self . item_details . has_serial_no == 1
and self . item_details . has_batch_no == 1
) :
self . set_batch_no_in_serial_nos ( )
2023-03-13 09:21:43 +00:00
2023-03-28 06:46:27 +00:00
if self . item_details . has_batch_no == 1 :
self . update_batch_qty ( )
2023-03-31 03:33:54 +00:00
def submit_serial_and_batch_bundle ( self ) :
doc = frappe . get_doc ( " Serial and Batch Bundle " , self . sle . serial_and_batch_bundle )
2023-05-27 13:48:03 +00:00
self . validate_actual_qty ( doc )
2023-03-31 03:33:54 +00:00
doc . flags . ignore_voucher_validation = True
doc . submit ( )
2023-03-13 09:21:43 +00:00
def set_warehouse_and_status_in_serial_nos ( self ) :
2023-03-31 03:33:54 +00:00
serial_nos = get_serial_nos ( self . sle . serial_and_batch_bundle )
2023-03-13 09:21:43 +00:00
warehouse = self . warehouse if self . sle . actual_qty > 0 else None
2023-03-16 07:28:48 +00:00
if not serial_nos :
return
2023-03-13 09:21:43 +00:00
2023-03-16 07:28:48 +00:00
sn_table = frappe . qb . DocType ( " Serial No " )
2023-03-13 09:21:43 +00:00
(
frappe . qb . update ( sn_table )
. set ( sn_table . warehouse , warehouse )
. set ( sn_table . status , " Active " if warehouse else " Inactive " )
. where ( sn_table . name . isin ( serial_nos ) )
) . run ( )
def set_batch_no_in_serial_nos ( self ) :
2023-03-21 05:24:41 +00:00
entries = frappe . get_all (
" Serial and Batch Entry " ,
2023-03-13 09:21:43 +00:00
fields = [ " serial_no " , " batch_no " ] ,
2023-03-16 07:28:48 +00:00
filters = { " parent " : self . sle . serial_and_batch_bundle } ,
2023-03-13 09:21:43 +00:00
)
batch_serial_nos = { }
2023-03-21 05:24:41 +00:00
for ledger in entries :
2023-03-13 09:21:43 +00:00
batch_serial_nos . setdefault ( ledger . batch_no , [ ] ) . append ( ledger . serial_no )
for batch_no , serial_nos in batch_serial_nos . items ( ) :
sn_table = frappe . qb . DocType ( " Serial No " )
(
frappe . qb . update ( sn_table )
. set ( sn_table . batch_no , batch_no )
. where ( sn_table . name . isin ( serial_nos ) )
) . run ( )
2023-03-28 06:46:27 +00:00
def update_batch_qty ( self ) :
from erpnext . stock . doctype . batch . batch import get_available_batches
batches = get_batch_nos ( self . sle . serial_and_batch_bundle )
batches_qty = get_available_batches (
frappe . _dict (
{ " item_code " : self . item_code , " warehouse " : self . warehouse , " batch_no " : list ( batches . keys ( ) ) }
)
)
2023-03-31 03:33:54 +00:00
for batch_no in batches :
frappe . db . set_value ( " Batch " , batch_no , " batch_qty " , batches_qty . get ( batch_no , 0 ) )
2023-03-28 06:46:27 +00:00
2023-03-13 09:21:43 +00:00
2023-04-03 06:56:12 +00:00
def get_serial_nos ( serial_and_batch_bundle , serial_nos = None ) :
2023-04-05 14:33:44 +00:00
if not serial_and_batch_bundle :
return [ ]
filters = { " parent " : serial_and_batch_bundle , " serial_no " : ( " is " , " set " ) }
2023-04-02 07:43:42 +00:00
if isinstance ( serial_and_batch_bundle , list ) :
filters = { " parent " : ( " in " , serial_and_batch_bundle ) }
2023-04-03 06:56:12 +00:00
if serial_nos :
filters [ " serial_no " ] = ( " in " , serial_nos )
2023-03-21 05:24:41 +00:00
entries = frappe . get_all ( " Serial and Batch Entry " , fields = [ " serial_no " ] , filters = filters )
2023-04-05 14:33:44 +00:00
if not entries :
return [ ]
return [ d . serial_no for d in entries if d . serial_no ]
2023-03-06 06:38:28 +00:00
2023-04-05 14:33:44 +00:00
def get_serial_nos_from_bundle ( serial_and_batch_bundle , serial_nos = None ) :
return get_serial_nos ( serial_and_batch_bundle , serial_nos = serial_nos )
2023-03-13 09:21:43 +00:00
2023-06-01 18:41:43 +00:00
def get_serial_or_batch_nos ( bundle ) :
return frappe . get_all ( " Serial and Batch Entry " , fields = [ " * " ] , filters = { " parent " : bundle } )
2023-03-23 06:11:20 +00:00
class SerialNoValuation ( DeprecatedSerialNoValuation ) :
2023-03-06 06:38:28 +00:00
def __init__ ( self , * * kwargs ) :
2023-03-13 09:21:43 +00:00
for key , value in kwargs . items ( ) :
2023-03-06 06:38:28 +00:00
setattr ( self , key , value )
2023-03-13 09:21:43 +00:00
self . calculate_stock_value_change ( )
self . calculate_valuation_rate ( )
def calculate_stock_value_change ( self ) :
2023-03-31 03:33:54 +00:00
if flt ( self . sle . actual_qty ) > 0 :
2023-03-13 09:21:43 +00:00
self . stock_value_change = frappe . get_cached_value (
" Serial and Batch Bundle " , self . sle . serial_and_batch_bundle , " total_amount "
)
else :
2023-03-21 05:24:41 +00:00
entries = self . get_serial_no_ledgers ( )
2023-03-13 09:21:43 +00:00
self . serial_no_incoming_rate = defaultdict ( float )
self . stock_value_change = 0.0
2023-03-06 06:38:28 +00:00
2023-03-21 05:24:41 +00:00
for ledger in entries :
2023-03-31 03:33:54 +00:00
self . stock_value_change + = ledger . incoming_rate
self . serial_no_incoming_rate [ ledger . serial_no ] + = ledger . incoming_rate
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
self . calculate_stock_value_from_deprecarated_ledgers ( )
def get_serial_no_ledgers ( self ) :
2023-03-06 06:38:28 +00:00
serial_nos = self . get_serial_nos ( )
2023-03-31 03:33:54 +00:00
bundle = frappe . qb . DocType ( " Serial and Batch Bundle " )
bundle_child = frappe . qb . DocType ( " Serial and Batch Entry " )
2023-03-06 06:38:28 +00:00
2023-03-31 03:33:54 +00:00
query = (
frappe . qb . from_ ( bundle )
. inner_join ( bundle_child )
. on ( bundle . name == bundle_child . parent )
. select (
bundle . name ,
bundle_child . serial_no ,
( bundle_child . incoming_rate * bundle_child . qty ) . as_ ( " incoming_rate " ) ,
)
. where (
( bundle . is_cancelled == 0 )
& ( bundle . docstatus == 1 )
& ( bundle_child . serial_no . isin ( serial_nos ) )
2023-04-02 07:43:42 +00:00
& ( bundle . type_of_transaction . isin ( [ " Inward " , " Outward " ] ) )
2023-03-31 03:33:54 +00:00
& ( bundle . item_code == self . sle . item_code )
& ( bundle_child . warehouse == self . sle . warehouse )
)
. orderby ( bundle . posting_date , bundle . posting_time , bundle . creation )
2023-03-06 06:38:28 +00:00
)
2023-03-31 03:33:54 +00:00
# Important to exclude the current voucher
2023-04-02 07:43:42 +00:00
if self . sle . voucher_no :
2023-03-31 03:33:54 +00:00
query = query . where ( bundle . voucher_no != self . sle . voucher_no )
if self . sle . posting_date :
if self . sle . posting_time is None :
self . sle . posting_time = nowtime ( )
timestamp_condition = CombineDatetime (
bundle . posting_date , bundle . posting_time
) < = CombineDatetime ( self . sle . posting_date , self . sle . posting_time )
query = query . where ( timestamp_condition )
return query . run ( as_dict = True )
2023-03-06 06:38:28 +00:00
def get_serial_nos ( self ) :
2023-03-13 09:21:43 +00:00
if self . sle . get ( " serial_nos " ) :
return self . sle . serial_nos
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
return get_serial_nos ( self . sle . serial_and_batch_bundle )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
def calculate_valuation_rate ( self ) :
if not hasattr ( self , " wh_data " ) :
return
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
new_stock_qty = self . wh_data . qty_after_transaction + self . sle . actual_qty
2023-03-06 06:38:28 +00:00
if new_stock_qty > 0 :
new_stock_value = (
self . wh_data . qty_after_transaction * self . wh_data . valuation_rate
2023-03-13 09:21:43 +00:00
) + self . stock_value_change
2023-03-06 06:38:28 +00:00
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 . wh_data . valuation_rate = new_stock_value / new_stock_qty
2023-03-13 09:21:43 +00:00
if (
not self . wh_data . valuation_rate and self . sle . voucher_detail_no and not self . is_rejected_entry ( )
) :
allow_zero_rate = self . sle_self . check_if_allow_zero_valuation_rate (
self . sle . voucher_type , self . sle . voucher_detail_no
2023-03-06 06:38:28 +00:00
)
if not allow_zero_rate :
2023-03-13 09:21:43 +00:00
self . wh_data . valuation_rate = self . sle_self . get_fallback_rate ( self . sle )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
self . wh_data . qty_after_transaction + = self . sle . actual_qty
self . wh_data . stock_value = flt ( self . wh_data . qty_after_transaction ) * flt (
self . wh_data . valuation_rate
2023-03-06 06:38:28 +00:00
)
2023-03-13 09:21:43 +00:00
def is_rejected_entry ( self ) :
return is_rejected ( self . sle . voucher_type , self . sle . voucher_detail_no , self . sle . warehouse )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
def get_incoming_rate ( self ) :
2023-03-16 07:28:48 +00:00
return abs ( flt ( self . stock_value_change ) / flt ( self . sle . actual_qty ) )
2023-03-13 09:21:43 +00:00
2023-04-02 07:43:42 +00:00
def get_incoming_rate_of_serial_no ( self , serial_no ) :
return self . serial_no_incoming_rate . get ( serial_no , 0.0 )
2023-03-13 09:21:43 +00:00
def is_rejected ( voucher_type , voucher_detail_no , warehouse ) :
if voucher_type in [ " Purchase Receipt " , " Purchase Invoice " ] :
return warehouse == frappe . get_cached_value (
voucher_type + " Item " , voucher_detail_no , " rejected_warehouse "
)
return False
2023-03-23 06:11:20 +00:00
class BatchNoValuation ( DeprecatedBatchNoValuation ) :
2023-03-13 09:21:43 +00:00
def __init__ ( self , * * kwargs ) :
for key , value in kwargs . items ( ) :
setattr ( self , key , value )
self . batch_nos = self . get_batch_nos ( )
2023-03-30 06:02:39 +00:00
self . prepare_batches ( )
2023-03-13 09:21:43 +00:00
self . calculate_avg_rate ( )
self . calculate_valuation_rate ( )
def calculate_avg_rate ( self ) :
2023-03-29 06:10:36 +00:00
if flt ( self . sle . actual_qty ) > 0 :
2023-03-13 09:21:43 +00:00
self . stock_value_change = frappe . get_cached_value (
" Serial and Batch Bundle " , self . sle . serial_and_batch_bundle , " total_amount "
2023-03-06 06:38:28 +00:00
)
2023-03-13 09:21:43 +00:00
else :
2023-03-21 05:24:41 +00:00
entries = self . get_batch_no_ledgers ( )
2023-06-01 10:38:49 +00:00
self . stock_value_change = 0.0
2023-03-13 09:21:43 +00:00
self . batch_avg_rate = defaultdict ( float )
2023-03-20 08:45:34 +00:00
self . available_qty = defaultdict ( float )
2023-03-30 06:02:39 +00:00
self . stock_value_differece = defaultdict ( float )
2023-03-28 06:46:27 +00:00
2023-03-21 05:24:41 +00:00
for ledger in entries :
2023-03-30 06:02:39 +00:00
self . stock_value_differece [ ledger . batch_no ] + = flt ( ledger . incoming_rate )
2023-03-20 08:45:34 +00:00
self . available_qty [ ledger . batch_no ] + = flt ( ledger . qty )
2023-03-13 09:21:43 +00:00
self . calculate_avg_rate_from_deprecarated_ledgers ( )
2023-06-01 10:38:49 +00:00
self . calculate_avg_rate_for_non_batchwise_valuation ( )
2023-03-13 09:21:43 +00:00
self . set_stock_value_difference ( )
2023-03-06 06:38:28 +00:00
2023-03-13 09:21:43 +00:00
def get_batch_no_ledgers ( self ) - > List [ dict ] :
2023-03-30 06:02:39 +00:00
if not self . batchwise_valuation_batches :
return [ ]
2023-03-13 09:21:43 +00:00
parent = frappe . qb . DocType ( " Serial and Batch Bundle " )
2023-03-21 05:24:41 +00:00
child = frappe . qb . DocType ( " Serial and Batch Entry " )
2023-03-06 06:38:28 +00:00
2023-03-28 06:46:27 +00:00
timestamp_condition = " "
if self . sle . posting_date and self . sle . posting_time :
timestamp_condition = CombineDatetime (
parent . posting_date , parent . posting_time
2023-03-31 03:33:54 +00:00
) < = CombineDatetime ( self . sle . posting_date , self . sle . posting_time )
2023-03-20 08:45:34 +00:00
2023-03-28 06:46:27 +00:00
query = (
2023-03-13 09:21:43 +00:00
frappe . qb . from_ ( parent )
. inner_join ( child )
. on ( parent . name == child . parent )
. select (
child . batch_no ,
Sum ( child . stock_value_difference ) . as_ ( " incoming_rate " ) ,
2023-03-16 07:28:48 +00:00
Sum ( child . qty ) . as_ ( " qty " ) ,
2023-03-13 09:21:43 +00:00
)
. where (
2023-03-30 06:02:39 +00:00
( child . batch_no . isin ( self . batchwise_valuation_batches ) )
2023-03-13 09:21:43 +00:00
& ( parent . warehouse == self . sle . warehouse )
& ( parent . item_code == self . sle . item_code )
2023-03-20 08:45:34 +00:00
& ( parent . docstatus == 1 )
2023-03-13 09:21:43 +00:00
& ( parent . is_cancelled == 0 )
2023-04-02 07:43:42 +00:00
& ( parent . type_of_transaction . isin ( [ " Inward " , " Outward " ] ) )
2023-03-13 09:21:43 +00:00
)
. groupby ( child . batch_no )
2023-03-28 06:46:27 +00:00
)
2023-03-31 03:33:54 +00:00
# Important to exclude the current voucher
if self . sle . voucher_no :
query = query . where ( parent . voucher_no != self . sle . voucher_no )
2023-03-29 06:10:36 +00:00
2023-03-28 06:46:27 +00:00
if timestamp_condition :
2023-03-29 06:10:36 +00:00
query = query . where ( timestamp_condition )
2023-03-28 06:46:27 +00:00
return query . run ( as_dict = True )
2023-03-13 09:21:43 +00:00
2023-03-30 06:02:39 +00:00
def prepare_batches ( self ) :
self . batches = self . batch_nos
if isinstance ( self . batch_nos , dict ) :
self . batches = list ( self . batch_nos . keys ( ) )
self . batchwise_valuation_batches = [ ]
self . non_batchwise_valuation_batches = [ ]
batches = frappe . get_all (
" Batch " , filters = { " name " : ( " in " , self . batches ) , " use_batchwise_valuation " : 1 } , fields = [ " name " ]
)
for batch in batches :
self . batchwise_valuation_batches . append ( batch . name )
self . non_batchwise_valuation_batches = list (
set ( self . batches ) - set ( self . batchwise_valuation_batches )
)
2023-03-13 09:21:43 +00:00
def get_batch_nos ( self ) - > list :
if self . sle . get ( " batch_nos " ) :
return self . sle . batch_nos
2023-03-06 06:38:28 +00:00
2023-03-28 06:46:27 +00:00
return get_batch_nos ( self . sle . serial_and_batch_bundle )
2023-03-13 09:21:43 +00:00
def set_stock_value_difference ( self ) :
for batch_no , ledger in self . batch_nos . items ( ) :
2023-06-01 10:38:49 +00:00
if batch_no in self . non_batchwise_valuation_batches :
continue
2023-03-31 03:33:54 +00:00
if not self . available_qty [ batch_no ] :
continue
2023-03-30 06:02:39 +00:00
self . batch_avg_rate [ batch_no ] = (
self . stock_value_differece [ batch_no ] / self . available_qty [ batch_no ]
)
# New Stock Value Difference
2023-03-16 07:28:48 +00:00
stock_value_change = self . batch_avg_rate [ batch_no ] * ledger . qty
2023-03-13 09:21:43 +00:00
self . stock_value_change + = stock_value_change
2023-06-01 10:38:49 +00:00
2023-03-13 09:21:43 +00:00
frappe . db . set_value (
2023-06-01 10:38:49 +00:00
" Serial and Batch Entry " ,
ledger . name ,
{
" stock_value_difference " : stock_value_change ,
" incoming_rate " : self . batch_avg_rate [ batch_no ] ,
} ,
2023-03-06 06:38:28 +00:00
)
2023-03-13 09:21:43 +00:00
def calculate_valuation_rate ( self ) :
if not hasattr ( self , " wh_data " ) :
return
2023-03-06 06:38:28 +00:00
self . wh_data . stock_value = round_off_if_near_zero (
2023-03-13 09:21:43 +00:00
self . wh_data . stock_value + self . stock_value_change
2023-03-06 06:38:28 +00:00
)
2023-03-13 09:21:43 +00:00
2023-03-29 06:10:36 +00:00
self . wh_data . qty_after_transaction + = self . sle . actual_qty
2023-03-06 06:38:28 +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
2023-03-13 09:21:43 +00:00
def get_incoming_rate ( self ) :
2023-03-31 03:33:54 +00:00
if not self . sle . actual_qty :
self . sle . actual_qty = self . get_actual_qty ( )
2023-03-16 07:28:48 +00:00
return abs ( flt ( self . stock_value_change ) / flt ( self . sle . actual_qty ) )
2023-03-20 17:26:06 +00:00
2023-03-31 03:33:54 +00:00
def get_actual_qty ( self ) :
total_qty = 0.0
for batch_no in self . available_qty :
total_qty + = self . available_qty [ batch_no ]
return total_qty
2023-03-20 17:26:06 +00:00
2023-03-28 06:46:27 +00:00
def get_batch_nos ( serial_and_batch_bundle ) :
2023-04-05 14:33:44 +00:00
if not serial_and_batch_bundle :
return frappe . _dict ( { } )
2023-03-28 06:46:27 +00:00
entries = frappe . get_all (
" Serial and Batch Entry " ,
fields = [ " batch_no " , " qty " , " name " ] ,
2023-04-05 14:33:44 +00:00
filters = { " parent " : serial_and_batch_bundle , " batch_no " : ( " is " , " set " ) } ,
2023-03-29 06:10:36 +00:00
order_by = " idx " ,
2023-03-28 06:46:27 +00:00
)
2023-04-05 14:33:44 +00:00
if not entries :
return frappe . _dict ( { } )
2023-03-28 06:46:27 +00:00
return { d . batch_no : d for d in entries }
2023-03-20 17:26:06 +00:00
def get_empty_batches_based_work_order ( work_order , item_code ) :
2023-03-23 06:11:20 +00:00
batches = get_batches_from_work_order ( work_order , item_code )
2023-03-20 17:26:06 +00:00
if not batches :
return batches
2023-03-23 06:11:20 +00:00
entries = get_batches_from_stock_entries ( work_order , item_code )
2023-03-20 17:26:06 +00:00
if not entries :
return batches
ids = [ d . serial_and_batch_bundle for d in entries if d . serial_and_batch_bundle ]
if ids :
set_batch_details_from_package ( ids , batches )
# Will be deprecated in v16
for d in entries :
if not d . batch_no :
continue
batches [ d . batch_no ] - = d . qty
return batches
2023-03-23 06:11:20 +00:00
def get_batches_from_work_order ( work_order , item_code ) :
2023-03-20 17:26:06 +00:00
return frappe . _dict (
frappe . get_all (
2023-03-23 06:11:20 +00:00
" Batch " ,
fields = [ " name " , " qty_to_produce " ] ,
filters = { " reference_name " : work_order , " item " : item_code } ,
as_list = 1 ,
2023-03-20 17:26:06 +00:00
)
)
2023-03-23 06:11:20 +00:00
def get_batches_from_stock_entries ( work_order , item_code ) :
2023-03-20 17:26:06 +00:00
entries = frappe . get_all (
" Stock Entry " ,
filters = { " work_order " : work_order , " docstatus " : 1 , " purpose " : " Manufacture " } ,
fields = [ " name " ] ,
)
return frappe . get_all (
" Stock Entry Detail " ,
fields = [ " batch_no " , " qty " , " serial_and_batch_bundle " ] ,
filters = {
" parent " : ( " in " , [ d . name for d in entries ] ) ,
" is_finished_item " : 1 ,
2023-03-23 06:11:20 +00:00
" item_code " : item_code ,
2023-03-20 17:26:06 +00:00
} ,
)
def set_batch_details_from_package ( ids , batches ) :
entries = frappe . get_all (
2023-03-21 05:24:41 +00:00
" Serial and Batch Entry " ,
2023-03-20 17:26:06 +00:00
filters = { " parent " : ( " in " , ids ) , " is_outward " : 0 } ,
fields = [ " batch_no " , " qty " ] ,
)
for d in entries :
batches [ d . batch_no ] - = d . qty
2023-03-23 06:11:20 +00:00
class SerialBatchCreation :
def __init__ ( self , args ) :
2023-03-28 06:46:27 +00:00
self . set ( args )
self . set_item_details ( )
2023-03-28 08:33:59 +00:00
self . set_other_details ( )
2023-03-28 06:46:27 +00:00
def set ( self , args ) :
self . __dict__ = { }
2023-03-23 06:11:20 +00:00
for key , value in args . items ( ) :
setattr ( self , key , value )
2023-03-28 06:46:27 +00:00
self . __dict__ [ key ] = value
def get ( self , key ) :
return self . __dict__ . get ( key )
def set_item_details ( self ) :
fields = [
" has_batch_no " ,
" has_serial_no " ,
" item_name " ,
" item_group " ,
" serial_no_series " ,
" create_new_batch " ,
" batch_number_series " ,
" description " ,
]
item_details = frappe . get_cached_value ( " Item " , self . item_code , fields , as_dict = 1 )
for key , value in item_details . items ( ) :
setattr ( self , key , value )
self . __dict__ . update ( item_details )
2023-03-23 06:11:20 +00:00
2023-03-28 08:33:59 +00:00
def set_other_details ( self ) :
if not self . get ( " posting_date " ) :
setattr ( self , " posting_date " , today ( ) )
self . __dict__ [ " posting_date " ] = self . posting_date
2023-03-29 06:10:36 +00:00
if not self . get ( " actual_qty " ) :
qty = self . get ( " qty " ) or self . get ( " total_qty " )
setattr ( self , " actual_qty " , qty )
self . __dict__ [ " actual_qty " ] = self . actual_qty
2023-03-23 06:11:20 +00:00
def duplicate_package ( self ) :
if not self . serial_and_batch_bundle :
return
id = self . serial_and_batch_bundle
package = frappe . get_doc ( " Serial and Batch Bundle " , id )
new_package = frappe . copy_doc ( package )
2023-04-02 07:43:42 +00:00
if self . get ( " returned_serial_nos " ) :
self . remove_returned_serial_nos ( new_package )
2023-03-31 03:33:54 +00:00
new_package . docstatus = 0
2023-03-23 06:11:20 +00:00
new_package . type_of_transaction = self . type_of_transaction
2023-03-31 03:33:54 +00:00
new_package . returned_against = self . get ( " returned_against " )
2023-03-23 06:11:20 +00:00
new_package . save ( )
self . serial_and_batch_bundle = new_package . name
2023-03-28 06:46:27 +00:00
2023-04-02 07:43:42 +00:00
def remove_returned_serial_nos ( self , package ) :
remove_list = [ ]
for d in package . entries :
if d . serial_no in self . returned_serial_nos :
remove_list . append ( d )
for d in remove_list :
package . remove ( d )
2023-03-28 06:46:27 +00:00
def make_serial_and_batch_bundle ( self ) :
doc = frappe . new_doc ( " Serial and Batch Bundle " )
valid_columns = doc . meta . get_valid_columns ( )
for key , value in self . __dict__ . items ( ) :
if key in valid_columns :
doc . set ( key , value )
if self . type_of_transaction == " Outward " :
self . set_auto_serial_batch_entries_for_outward ( )
2023-03-31 03:33:54 +00:00
elif self . type_of_transaction == " Inward " :
2023-03-28 06:46:27 +00:00
self . set_auto_serial_batch_entries_for_inward ( )
2023-04-05 14:33:44 +00:00
self . add_serial_nos_for_batch_item ( )
2023-03-28 06:46:27 +00:00
self . set_serial_batch_entries ( doc )
2023-04-02 07:43:42 +00:00
if not doc . get ( " entries " ) :
return frappe . _dict ( { } )
2023-03-28 06:46:27 +00:00
doc . save ( )
2023-06-05 11:26:29 +00:00
self . validate_qty ( doc )
2023-03-28 06:46:27 +00:00
if not hasattr ( self , " do_not_submit " ) or not self . do_not_submit :
2023-03-29 06:10:36 +00:00
doc . flags . ignore_voucher_validation = True
2023-03-28 06:46:27 +00:00
doc . submit ( )
return doc
2023-04-05 14:33:44 +00:00
def add_serial_nos_for_batch_item ( self ) :
if not ( self . has_serial_no and self . has_batch_no ) :
return
if not self . get ( " serial_nos " ) and self . get ( " batches " ) :
batches = list ( self . get ( " batches " ) . keys ( ) )
if len ( batches ) == 1 :
self . batch_no = batches [ 0 ]
self . serial_nos = self . get_auto_created_serial_nos ( )
2023-04-03 06:56:12 +00:00
def update_serial_and_batch_entries ( self ) :
doc = frappe . get_doc ( " Serial and Batch Bundle " , self . serial_and_batch_bundle )
doc . type_of_transaction = self . type_of_transaction
doc . set ( " entries " , [ ] )
self . set_auto_serial_batch_entries_for_outward ( )
self . set_serial_batch_entries ( doc )
if not doc . get ( " entries " ) :
return frappe . _dict ( { } )
doc . save ( )
return doc
2023-06-05 11:26:29 +00:00
def validate_qty ( self , doc ) :
if doc . type_of_transaction == " Outward " :
precision = doc . precision ( " total_qty " )
total_qty = abs ( flt ( doc . total_qty , precision ) )
required_qty = abs ( flt ( self . actual_qty , precision ) )
if required_qty - total_qty > 0 :
msg = f " For the item { bold ( doc . item_code ) } , the Avaliable qty { bold ( total_qty ) } is less than the Required Qty { bold ( required_qty ) } in the warehouse { bold ( doc . warehouse ) } . Please add sufficient qty in the warehouse. "
frappe . throw ( msg , title = _ ( " Insufficient Stock " ) )
2023-03-28 06:46:27 +00:00
def set_auto_serial_batch_entries_for_outward ( self ) :
from erpnext . stock . doctype . batch . batch import get_available_batches
from erpnext . stock . doctype . serial_no . serial_no import get_serial_nos_for_outward
kwargs = frappe . _dict (
{
" item_code " : self . item_code ,
" warehouse " : self . warehouse ,
2023-03-31 03:33:54 +00:00
" qty " : abs ( self . actual_qty ) if self . actual_qty else 0 ,
2023-03-28 06:46:27 +00:00
" based_on " : frappe . db . get_single_value ( " Stock Settings " , " pick_serial_and_batch_based_on " ) ,
}
)
2023-04-03 06:56:12 +00:00
if self . get ( " ignore_serial_nos " ) :
kwargs [ " ignore_serial_nos " ] = self . ignore_serial_nos
2023-03-28 06:46:27 +00:00
if self . has_serial_no and not self . get ( " serial_nos " ) :
self . serial_nos = get_serial_nos_for_outward ( kwargs )
2023-03-28 08:33:59 +00:00
elif not self . has_serial_no and self . has_batch_no and not self . get ( " batches " ) :
2023-03-28 06:46:27 +00:00
self . batches = get_available_batches ( kwargs )
def set_auto_serial_batch_entries_for_inward ( self ) :
2023-03-31 03:33:54 +00:00
if ( self . get ( " batches " ) and self . has_batch_no ) or (
self . get ( " serial_nos " ) and self . has_serial_no
) :
return
2023-03-28 06:46:27 +00:00
self . batch_no = None
if self . has_batch_no :
self . batch_no = self . create_batch ( )
if self . has_serial_no :
self . serial_nos = self . get_auto_created_serial_nos ( )
else :
2023-03-29 06:10:36 +00:00
self . batches = frappe . _dict ( { self . batch_no : abs ( self . actual_qty ) } )
2023-03-28 06:46:27 +00:00
def set_serial_batch_entries ( self , doc ) :
if self . get ( " serial_nos " ) :
serial_no_wise_batch = frappe . _dict ( { } )
if self . has_batch_no :
serial_no_wise_batch = self . get_serial_nos_batch ( self . serial_nos )
qty = - 1 if self . type_of_transaction == " Outward " else 1
for serial_no in self . serial_nos :
doc . append (
" entries " ,
{
" serial_no " : serial_no ,
" qty " : qty ,
" batch_no " : serial_no_wise_batch . get ( serial_no ) or self . get ( " batch_no " ) ,
" incoming_rate " : self . get ( " incoming_rate " ) ,
} ,
)
2023-04-05 14:33:44 +00:00
elif self . get ( " batches " ) :
2023-03-28 06:46:27 +00:00
for batch_no , batch_qty in self . batches . items ( ) :
doc . append (
" entries " ,
{
" batch_no " : batch_no ,
" qty " : batch_qty * ( - 1 if self . type_of_transaction == " Outward " else 1 ) ,
" incoming_rate " : self . get ( " incoming_rate " ) ,
} ,
)
def get_serial_nos_batch ( self , serial_nos ) :
return frappe . _dict (
frappe . get_all (
" Serial No " ,
fields = [ " name " , " batch_no " ] ,
filters = { " name " : ( " in " , serial_nos ) } ,
as_list = 1 ,
)
)
def create_batch ( self ) :
from erpnext . stock . doctype . batch . batch import make_batch
return make_batch (
frappe . _dict (
{
2023-03-29 06:10:36 +00:00
" item " : self . get ( " item_code " ) ,
" reference_doctype " : self . get ( " voucher_type " ) ,
" reference_name " : self . get ( " voucher_no " ) ,
2023-03-28 06:46:27 +00:00
}
)
)
def get_auto_created_serial_nos ( self ) :
sr_nos = [ ]
serial_nos_details = [ ]
2023-03-31 03:33:54 +00:00
if not self . serial_no_series :
msg = f " Please set Serial No Series in the item { self . item_code } or create Serial and Batch Bundle manually. "
frappe . throw ( _ ( msg ) )
2023-03-29 06:10:36 +00:00
for i in range ( abs ( cint ( self . actual_qty ) ) ) :
2023-03-28 06:46:27 +00:00
serial_no = make_autoname ( self . serial_no_series , " Serial No " )
sr_nos . append ( serial_no )
serial_nos_details . append (
(
serial_no ,
serial_no ,
now ( ) ,
now ( ) ,
frappe . session . user ,
frappe . session . user ,
self . warehouse ,
self . company ,
self . item_code ,
self . item_name ,
self . description ,
" Active " ,
self . batch_no ,
)
)
if serial_nos_details :
fields = [
" name " ,
" serial_no " ,
" creation " ,
" modified " ,
" owner " ,
" modified_by " ,
" warehouse " ,
" company " ,
" item_code " ,
" item_name " ,
" description " ,
" status " ,
" batch_no " ,
]
frappe . db . bulk_insert ( " Serial No " , fields = fields , values = set ( serial_nos_details ) )
return sr_nos
def get_serial_or_batch_items ( items ) :
serial_or_batch_items = frappe . get_all (
" Item " ,
filters = { " name " : ( " in " , [ d . item_code for d in items ] ) } ,
or_filters = { " has_serial_no " : 1 , " has_batch_no " : 1 } ,
)
if not serial_or_batch_items :
return
else :
serial_or_batch_items = [ d . name for d in serial_or_batch_items ]
return serial_or_batch_items