@@ -3856,6 +3856,122 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
38563856 return bool (deleted_children )
38573857
38583858
3859+ def get_allow_zero_qty (parent_doctype : str ) -> bool :
3860+ if parent_doctype == "Sales Order" :
3861+ return frappe .db .get_single_value ("Selling Settings" , "allow_zero_qty_in_sales_order" ) or False
3862+ if parent_doctype == "Purchase Order" :
3863+ return frappe .db .get_single_value ("Buying Settings" , "allow_zero_qty_in_purchase_order" ) or False
3864+ return False
3865+
3866+
3867+ def get_child_item_change_state (parent_doctype : str , child_item , new_data ) -> frappe ._dict :
3868+ prev_rate , new_rate = flt (child_item .get ("rate" )), flt (new_data .get ("rate" ))
3869+ prev_qty , new_qty = flt (child_item .get ("qty" )), flt (new_data .get ("qty" ))
3870+ prev_fg_qty , new_fg_qty = flt (child_item .get ("fg_item_qty" )), flt (new_data .get ("fg_item_qty" ))
3871+ prev_con_fac , new_con_fac = (
3872+ flt (child_item .get ("conversion_factor" )),
3873+ flt (new_data .get ("conversion_factor" )),
3874+ )
3875+
3876+ if parent_doctype == "Sales Order" :
3877+ prev_date , new_date = child_item .get ("delivery_date" ), new_data .get ("delivery_date" )
3878+ elif parent_doctype == "Purchase Order" :
3879+ prev_date , new_date = child_item .get ("schedule_date" ), new_data .get ("schedule_date" )
3880+ else :
3881+ prev_date , new_date = None , None
3882+
3883+ date_unchanged = (
3884+ (prev_date == getdate (new_date ) if prev_date and new_date else False )
3885+ if parent_doctype not in ["Quotation" , "Supplier Quotation" ]
3886+ else None
3887+ )
3888+
3889+ return frappe ._dict (
3890+ rate_unchanged = prev_rate == new_rate ,
3891+ qty_unchanged = prev_qty == new_qty ,
3892+ fg_qty_unchanged = prev_fg_qty == new_fg_qty ,
3893+ uom_unchanged = child_item .get ("uom" ) == new_data .get ("uom" ),
3894+ conversion_factor_unchanged = prev_con_fac == new_con_fac ,
3895+ date_unchanged = date_unchanged ,
3896+ description_unchanged = child_item .get ("description" ) == new_data .get ("description" ),
3897+ )
3898+
3899+
3900+ def update_child_item_rate_and_discount (
3901+ parent_doctype : str , child_item , new_data , allow_zero_qty : bool
3902+ ) -> None :
3903+ rate_precision = child_item .precision ("rate" ) or 2
3904+ qty_precision = child_item .precision ("qty" ) or 2
3905+
3906+ prev_rate , new_rate = flt (child_item .get ("rate" )), flt (new_data .get ("rate" ))
3907+ rate_unchanged = prev_rate == new_rate
3908+ if not rate_unchanged and not child_item .get ("qty" ) and allow_zero_qty :
3909+ frappe .throw (_ ("Rate of '{}' items cannot be changed" ).format (frappe .bold (_ ("Unit Price" ))))
3910+
3911+ # Amount cannot be lesser than billed amount, except for negative amounts
3912+ row_rate = flt (new_data .get ("rate" ), rate_precision )
3913+
3914+ if parent_doctype in ["Purchase Order" , "Sales Order" ]:
3915+ amount_below_billed_amt = flt (child_item .billed_amt , rate_precision ) > flt (
3916+ row_rate * flt (new_data .get ("qty" ), qty_precision ), rate_precision
3917+ )
3918+ if amount_below_billed_amt and row_rate > 0.0 :
3919+ frappe .throw (
3920+ _ (
3921+ "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
3922+ ).format (child_item .idx , child_item .item_code )
3923+ )
3924+
3925+ child_item .rate = row_rate
3926+
3927+ if parent_doctype not in ["Sales Order" , "Purchase Order" ] or not flt (child_item .price_list_rate ):
3928+ return
3929+
3930+ if flt (child_item .rate ) > flt (child_item .price_list_rate ):
3931+ # if rate is greater than price_list_rate, set margin or set discount
3932+ child_item .discount_percentage = 0
3933+ child_item .margin_type = "Amount"
3934+ child_item .margin_rate_or_amount = flt (
3935+ child_item .rate - child_item .price_list_rate ,
3936+ child_item .precision ("margin_rate_or_amount" ),
3937+ )
3938+ child_item .rate_with_margin = child_item .rate
3939+ else :
3940+ child_item .discount_percentage = flt (
3941+ (1 - flt (child_item .rate ) / flt (child_item .price_list_rate )) * 100.0 ,
3942+ child_item .precision ("discount_percentage" ),
3943+ )
3944+ child_item .discount_amount = flt (child_item .price_list_rate ) - flt (child_item .rate )
3945+ child_item .margin_type = ""
3946+ child_item .margin_rate_or_amount = 0
3947+ child_item .rate_with_margin = 0
3948+
3949+
3950+ def update_child_item_uom_and_weight (child_item , new_data ) -> None :
3951+ conv_fac_precision = child_item .precision ("conversion_factor" ) or 2
3952+
3953+ if new_data .get ("conversion_factor" ):
3954+ if child_item .stock_uom == child_item .uom :
3955+ child_item .conversion_factor = 1
3956+ else :
3957+ child_item .conversion_factor = flt (new_data .get ("conversion_factor" ), conv_fac_precision )
3958+
3959+ if new_data .get ("uom" ):
3960+ child_item .uom = new_data .get ("uom" )
3961+ conversion_factor = flt (
3962+ get_conversion_factor (child_item .item_code , child_item .uom ).get ("conversion_factor" )
3963+ )
3964+ child_item .conversion_factor = (
3965+ flt (new_data .get ("conversion_factor" ), conv_fac_precision ) or conversion_factor
3966+ )
3967+
3968+ if child_item .get ("total_weight" ) and child_item .get ("weight_per_unit" ):
3969+ child_item .total_weight = flt (
3970+ child_item .weight_per_unit * child_item .qty * child_item .conversion_factor ,
3971+ child_item .precision ("total_weight" ),
3972+ )
3973+
3974+
38593975@frappe .whitelist ()
38603976def update_child_qty_rate (
38613977 parent_doctype : str , trans_items : str , parent_doctype_name : str , child_docname : str = "items"
@@ -3904,15 +4020,8 @@ def get_new_child_item(item_row):
39044020 child_doctype = parent_doctype + " Item"
39054021 return set_order_defaults (parent_doctype , parent_doctype_name , child_doctype , child_docname , item_row )
39064022
3907- def is_allowed_zero_qty ():
3908- if parent_doctype == "Sales Order" :
3909- return frappe .db .get_single_value ("Selling Settings" , "allow_zero_qty_in_sales_order" ) or False
3910- elif parent_doctype == "Purchase Order" :
3911- return frappe .db .get_single_value ("Buying Settings" , "allow_zero_qty_in_purchase_order" ) or False
3912- return False
3913-
39144023 def validate_quantity_and_rate (child_item , new_data ):
3915- if not flt (new_data .get ("qty" )) and not is_allowed_zero_qty () :
4024+ if not flt (new_data .get ("qty" )) and not allow_zero_qty :
39164025 frappe .throw (
39174026 _ ("Row #{0}:Quantity for Item {1} cannot be zero." ).format (
39184027 new_data .get ("idx" ), frappe .bold (new_data .get ("item_code" ))
@@ -4004,6 +4113,7 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40044113 any_conversion_factor_changed = False
40054114
40064115 parent = frappe .get_doc (parent_doctype , parent_doctype_name )
4116+ allow_zero_qty = get_allow_zero_qty (parent_doctype )
40074117
40084118 check_doc_permissions (parent , "write" )
40094119
@@ -4034,42 +4144,10 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40344144 check_doc_permissions (parent , "write" )
40354145 child_item = frappe .get_doc (parent_doctype + " Item" , d .get ("docname" ))
40364146
4037- prev_rate , new_rate = flt (child_item .get ("rate" )), flt (d .get ("rate" ))
4038- prev_qty , new_qty = flt (child_item .get ("qty" )), flt (d .get ("qty" ))
4039- prev_fg_qty , new_fg_qty = flt (child_item .get ("fg_item_qty" )), flt (d .get ("fg_item_qty" ))
4040- prev_con_fac , new_con_fac = (
4041- flt (child_item .get ("conversion_factor" )),
4042- flt (d .get ("conversion_factor" )),
4043- )
4044- prev_uom , new_uom = child_item .get ("uom" ), d .get ("uom" )
4045-
4046- if parent_doctype == "Sales Order" :
4047- prev_date , new_date = child_item .get ("delivery_date" ), d .get ("delivery_date" )
4048- elif parent_doctype == "Purchase Order" :
4049- prev_date , new_date = child_item .get ("schedule_date" ), d .get ("schedule_date" )
4050-
4051- prev_description , new_description = (child_item .get ("description" ), d .get ("description" ))
4052- description_unchanged = prev_description == new_description
4053- rate_unchanged = prev_rate == new_rate
4054- qty_unchanged = prev_qty == new_qty
4055- fg_qty_unchanged = prev_fg_qty == new_fg_qty
4056- uom_unchanged = prev_uom == new_uom
4057- conversion_factor_unchanged = prev_con_fac == new_con_fac
4058- any_conversion_factor_changed |= not conversion_factor_unchanged
4059- date_unchanged = (
4060- (prev_date == getdate (new_date ) if prev_date and new_date else False )
4061- if parent_doctype not in ["Quotation" , "Supplier Quotation" ]
4062- else None
4063- ) # in case of delivery note etc
4064- if (
4065- rate_unchanged
4066- and qty_unchanged
4067- and fg_qty_unchanged
4068- and conversion_factor_unchanged
4069- and uom_unchanged
4070- and date_unchanged
4071- and description_unchanged
4072- ):
4147+ change_state = get_child_item_change_state (parent_doctype , child_item , d )
4148+ rate_unchanged = change_state .rate_unchanged
4149+ any_conversion_factor_changed |= not change_state .conversion_factor_unchanged
4150+ if all (change_state .values ()):
40734151 continue
40744152
40754153 validate_quantity_and_rate (child_item , d )
@@ -4090,52 +4168,8 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40904168
40914169 child_item .qty = flt (d .get ("qty" ))
40924170 child_item .description = d .get ("description" )
4093- rate_precision = child_item .precision ("rate" ) or 2
4094- conv_fac_precision = child_item .precision ("conversion_factor" ) or 2
4095- qty_precision = child_item .precision ("qty" ) or 2
4096-
4097- prev_rate , new_rate = flt (child_item .get ("rate" )), flt (d .get ("rate" ))
4098- rate_unchanged = prev_rate == new_rate
4099- if not rate_unchanged and not child_item .get ("qty" ) and is_allowed_zero_qty ():
4100- frappe .throw (_ ("Rate of '{}' items cannot be changed" ).format (frappe .bold (_ ("Unit Price" ))))
4101- # Amount cannot be lesser than billed amount, except for negative amounts
4102- row_rate = flt (d .get ("rate" ), rate_precision )
4103-
4104- if parent_doctype in ["Purchase Order" , "Sales Order" ]:
4105- amount_below_billed_amt = flt (child_item .billed_amt , rate_precision ) > flt (
4106- row_rate * flt (d .get ("qty" ), qty_precision ), rate_precision
4107- )
4108- if amount_below_billed_amt and row_rate > 0.0 :
4109- frappe .throw (
4110- _ (
4111- "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
4112- ).format (child_item .idx , child_item .item_code )
4113- )
4114- else :
4115- child_item .rate = row_rate
4116- else :
4117- child_item .rate = row_rate
4118-
4119- if d .get ("conversion_factor" ):
4120- if child_item .stock_uom == child_item .uom :
4121- child_item .conversion_factor = 1
4122- else :
4123- child_item .conversion_factor = flt (d .get ("conversion_factor" ), conv_fac_precision )
4124-
4125- if d .get ("uom" ):
4126- child_item .uom = d .get ("uom" )
4127- conversion_factor = flt (
4128- get_conversion_factor (child_item .item_code , child_item .uom ).get ("conversion_factor" )
4129- )
4130- child_item .conversion_factor = (
4131- flt (d .get ("conversion_factor" ), conv_fac_precision ) or conversion_factor
4132- )
4133-
4134- if child_item .get ("total_weight" ) and child_item .get ("weight_per_unit" ):
4135- child_item .total_weight = flt (
4136- child_item .weight_per_unit * child_item .qty * child_item .conversion_factor ,
4137- child_item .precision ("total_weight" ),
4138- )
4171+ update_child_item_rate_and_discount (parent_doctype , child_item , d , allow_zero_qty )
4172+ update_child_item_uom_and_weight (child_item , d )
41394173
41404174 if d .get ("delivery_date" ) and parent_doctype == "Sales Order" :
41414175 child_item .delivery_date = d .get ("delivery_date" )
@@ -4146,28 +4180,6 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
41464180 if d .get ("bom_no" ) and parent_doctype == "Sales Order" :
41474181 child_item .bom_no = d .get ("bom_no" )
41484182
4149- if parent_doctype in ["Sales Order" , "Purchase Order" ]:
4150- if flt (child_item .price_list_rate ):
4151- if flt (child_item .rate ) > flt (child_item .price_list_rate ):
4152- # if rate is greater than price_list_rate, set margin
4153- # or set discount
4154- child_item .discount_percentage = 0
4155- child_item .margin_type = "Amount"
4156- child_item .margin_rate_or_amount = flt (
4157- child_item .rate - child_item .price_list_rate ,
4158- child_item .precision ("margin_rate_or_amount" ),
4159- )
4160- child_item .rate_with_margin = child_item .rate
4161- else :
4162- child_item .discount_percentage = flt (
4163- (1 - flt (child_item .rate ) / flt (child_item .price_list_rate )) * 100.0 ,
4164- child_item .precision ("discount_percentage" ),
4165- )
4166- child_item .discount_amount = flt (child_item .price_list_rate ) - flt (child_item .rate )
4167- child_item .margin_type = ""
4168- child_item .margin_rate_or_amount = 0
4169- child_item .rate_with_margin = 0
4170-
41714183 child_item .flags .ignore_validate_update_after_submit = True
41724184 if new_child_flag :
41734185 parent .load_from_db ()
0 commit comments