Skip to content

Commit 96c4540

Browse files
committed
refactor: update_child_qty_rate function
1 parent 06ffe52 commit 96c4540

3 files changed

Lines changed: 210 additions & 112 deletions

File tree

erpnext/controllers/accounts_controller.py

Lines changed: 147 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -3856,6 +3856,142 @@ 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 == "Quotation":
3861+
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_quotation") or False
3862+
if parent_doctype == "Sales Order":
3863+
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
3864+
if parent_doctype == "Purchase Order":
3865+
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
3866+
return False
3867+
3868+
3869+
def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict:
3870+
prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate"))
3871+
prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty"))
3872+
prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty"))
3873+
prev_con_fac, new_con_fac = (
3874+
flt(child_item.get("conversion_factor")),
3875+
flt(new_data.get("conversion_factor")),
3876+
)
3877+
3878+
if parent_doctype == "Sales Order":
3879+
prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date")
3880+
elif parent_doctype == "Purchase Order":
3881+
prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date")
3882+
else:
3883+
prev_date, new_date = None, None
3884+
3885+
if parent_doctype in ["Quotation", "Supplier Quotation"]:
3886+
date_unchanged = True
3887+
else:
3888+
prev_date = getdate(prev_date) if prev_date else None
3889+
new_date = getdate(new_date) if new_date else None
3890+
date_unchanged = prev_date == new_date
3891+
3892+
return frappe._dict(
3893+
rate_unchanged=prev_rate == new_rate,
3894+
qty_unchanged=prev_qty == new_qty,
3895+
fg_qty_unchanged=prev_fg_qty == new_fg_qty,
3896+
uom_unchanged=child_item.get("uom") == new_data.get("uom"),
3897+
conversion_factor_unchanged=prev_con_fac == new_con_fac,
3898+
date_unchanged=date_unchanged,
3899+
description_unchanged=child_item.get("description") == new_data.get("description"),
3900+
bom_unchanged=(parent_doctype != "Sales Order" or child_item.get("bom_no") == new_data.get("bom_no")),
3901+
)
3902+
3903+
3904+
def is_child_item_unchanged(change_state: frappe._dict) -> bool:
3905+
return (
3906+
change_state.rate_unchanged
3907+
and change_state.qty_unchanged
3908+
and change_state.fg_qty_unchanged
3909+
and change_state.conversion_factor_unchanged
3910+
and change_state.uom_unchanged
3911+
and change_state.date_unchanged
3912+
and change_state.description_unchanged
3913+
and change_state.bom_unchanged
3914+
)
3915+
3916+
3917+
def update_child_item_rate_and_discount(
3918+
parent_doctype: str, child_item, new_data, allow_zero_qty: bool, rate_unchanged: bool | None = None
3919+
) -> None:
3920+
rate_precision = child_item.precision("rate") or 2
3921+
qty_precision = child_item.precision("qty") or 2
3922+
3923+
if rate_unchanged is None:
3924+
prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate"))
3925+
rate_unchanged = prev_rate == new_rate
3926+
3927+
if not rate_unchanged and not child_item.get("qty") and allow_zero_qty:
3928+
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
3929+
3930+
# Amount cannot be lesser than billed amount, except for negative amounts
3931+
row_rate = flt(new_data.get("rate"), rate_precision)
3932+
3933+
if parent_doctype in ["Purchase Order", "Sales Order"]:
3934+
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
3935+
row_rate * flt(new_data.get("qty"), qty_precision), rate_precision
3936+
)
3937+
if amount_below_billed_amt and row_rate > 0.0:
3938+
frappe.throw(
3939+
_(
3940+
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
3941+
).format(child_item.idx, child_item.item_code)
3942+
)
3943+
3944+
child_item.rate = row_rate
3945+
3946+
if parent_doctype not in ["Sales Order", "Purchase Order"] or not flt(child_item.price_list_rate):
3947+
return
3948+
3949+
if flt(child_item.rate) > flt(child_item.price_list_rate):
3950+
# if rate is greater than price_list_rate, set margin or set discount
3951+
child_item.discount_percentage = 0
3952+
child_item.discount_amount = 0
3953+
child_item.margin_type = "Amount"
3954+
child_item.margin_rate_or_amount = flt(
3955+
child_item.rate - child_item.price_list_rate,
3956+
child_item.precision("margin_rate_or_amount"),
3957+
)
3958+
child_item.rate_with_margin = child_item.rate
3959+
else:
3960+
child_item.discount_percentage = flt(
3961+
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
3962+
child_item.precision("discount_percentage"),
3963+
)
3964+
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
3965+
child_item.margin_type = ""
3966+
child_item.margin_rate_or_amount = 0
3967+
child_item.rate_with_margin = 0
3968+
3969+
3970+
def update_child_item_uom_and_weight(child_item, new_data) -> None:
3971+
conv_fac_precision = child_item.precision("conversion_factor") or 2
3972+
3973+
if new_data.get("conversion_factor"):
3974+
if child_item.stock_uom == child_item.uom:
3975+
child_item.conversion_factor = 1
3976+
else:
3977+
child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision)
3978+
3979+
if new_data.get("uom"):
3980+
child_item.uom = new_data.get("uom")
3981+
conversion_factor = flt(
3982+
get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")
3983+
)
3984+
child_item.conversion_factor = (
3985+
flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor
3986+
)
3987+
3988+
if child_item.get("total_weight") and child_item.get("weight_per_unit"):
3989+
child_item.total_weight = flt(
3990+
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
3991+
child_item.precision("total_weight"),
3992+
)
3993+
3994+
38593995
@frappe.whitelist()
38603996
def update_child_qty_rate(
38613997
parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
@@ -3904,15 +4040,8 @@ def get_new_child_item(item_row):
39044040
child_doctype = parent_doctype + " Item"
39054041
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
39064042

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-
39144043
def validate_quantity_and_rate(child_item, new_data):
3915-
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
4044+
if not flt(new_data.get("qty")) and not allow_zero_qty:
39164045
frappe.throw(
39174046
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
39184047
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
@@ -4004,6 +4133,7 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40044133
any_conversion_factor_changed = False
40054134

40064135
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
4136+
allow_zero_qty = get_allow_zero_qty(parent_doctype)
40074137

40084138
check_doc_permissions(parent, "write")
40094139

@@ -4020,6 +4150,7 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40204150

40214151
for d in data:
40224152
new_child_flag = False
4153+
rate_unchanged = None
40234154

40244155
if not d.get("item_code"):
40254156
# ignore empty rows
@@ -4034,42 +4165,10 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40344165
check_doc_permissions(parent, "write")
40354166
child_item = frappe.get_doc(parent_doctype + " Item", d.get("docname"))
40364167

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-
):
4168+
change_state = get_child_item_change_state(parent_doctype, child_item, d)
4169+
rate_unchanged = change_state.rate_unchanged
4170+
any_conversion_factor_changed |= not change_state.conversion_factor_unchanged
4171+
if is_child_item_unchanged(change_state):
40734172
continue
40744173

40754174
validate_quantity_and_rate(child_item, d)
@@ -4090,52 +4189,10 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
40904189

40914190
child_item.qty = flt(d.get("qty"))
40924191
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-
)
4192+
update_child_item_rate_and_discount(
4193+
parent_doctype, child_item, d, allow_zero_qty, rate_unchanged=rate_unchanged
4194+
)
4195+
update_child_item_uom_and_weight(child_item, d)
41394196

41404197
if d.get("delivery_date") and parent_doctype == "Sales Order":
41414198
child_item.delivery_date = d.get("delivery_date")
@@ -4146,28 +4203,6 @@ def validate_fg_item_for_subcontracting(new_data, is_new):
41464203
if d.get("bom_no") and parent_doctype == "Sales Order":
41474204
child_item.bom_no = d.get("bom_no")
41484205

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-
41714206
child_item.flags.ignore_validate_update_after_submit = True
41724207
if new_child_flag:
41734208
parent.load_from_db()

erpnext/selling/doctype/quotation/test_quotation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,29 @@ def test_quotation_zero_qty(self):
175175
qo.save()
176176
self.assertEqual(qo.items[0].qty, 0)
177177

178+
def test_update_child_qty_rate_allows_zero_qty_for_quotation(self):
179+
qo = make_quotation(qty=1)
180+
item = qo.items[0]
181+
trans_item = json.dumps(
182+
[
183+
{
184+
"item_code": item.item_code,
185+
"rate": item.rate,
186+
"qty": 0,
187+
"docname": item.name,
188+
"uom": item.uom,
189+
"conversion_factor": item.conversion_factor,
190+
"description": item.description,
191+
}
192+
]
193+
)
194+
195+
with change_settings("Selling Settings", {"allow_zero_qty_in_quotation": 1}):
196+
update_child_qty_rate("Quotation", trans_item, qo.name)
197+
198+
qo.reload()
199+
self.assertEqual(qo.items[0].qty, 0)
200+
178201
def test_make_quotation_without_terms(self):
179202
quotation = make_quotation(do_not_save=1)
180203
self.assertFalse(quotation.get("payment_schedule"))

erpnext/selling/doctype/sales_order/test_sales_order.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,46 @@ def test_update_child_with_precision(self):
668668
self.assertEqual(so.items[0].rate, 200.34669)
669669
make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
670670

671+
def test_update_child_updates_bom_when_other_values_are_unchanged(self):
672+
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
673+
674+
fg_item = make_item("_Test Update Child BOM FG", {"is_stock_item": 1}).name
675+
rm_item = make_item("_Test Update Child BOM RM", {"is_stock_item": 1}).name
676+
bom = make_bom(item=fg_item, raw_materials=[rm_item])
677+
new_bom = make_bom(item=fg_item, raw_materials=[rm_item])
678+
so = make_sales_order(
679+
item_list=[
680+
{
681+
"item_code": fg_item,
682+
"warehouse": "_Test Warehouse - _TC",
683+
"qty": 1,
684+
"rate": 100,
685+
"bom_no": bom.name,
686+
}
687+
]
688+
)
689+
item = so.items[0]
690+
trans_item = json.dumps(
691+
[
692+
{
693+
"item_code": item.item_code,
694+
"rate": item.rate,
695+
"qty": item.qty,
696+
"docname": item.name,
697+
"uom": item.uom,
698+
"conversion_factor": item.conversion_factor,
699+
"description": item.description,
700+
"delivery_date": item.delivery_date,
701+
"bom_no": new_bom.name,
702+
}
703+
]
704+
)
705+
706+
update_child_qty_rate("Sales Order", trans_item, so.name)
707+
708+
so.reload()
709+
self.assertEqual(so.items[0].bom_no, new_bom.name)
710+
671711
def test_update_child_perm(self):
672712
so = make_sales_order(item_code="_Test Item", qty=4)
673713

0 commit comments

Comments
 (0)