mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-17 05:45:11 +00:00
* feat: update item button addition for quotation (#50976)
* feat: update item button addition for quotation
* feat: update item button addition for supplier quotation
* fix: test case
---------
Co-authored-by: Nishka Gosalia <nishkagosalia@Nishkas-MacBook-Air.local>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit f4c0611cc5)
* chore: resolve conflicts
* chore: resolve conflicts
* chore: resolve conflicts
* chore: resolve conflicts
* chore: resolve conflicts
* chore: resolve conflicts
---------
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
This commit is contained in:
committed by
Mihir Kandoi
parent
ee7602e752
commit
800e38453b
@@ -30,6 +30,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
|||||||
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||||
|
|
||||||
|
this.frm.add_custom_button(__("Update Items"), () => {
|
||||||
|
erpnext.utils.update_child_items({
|
||||||
|
frm: this.frm,
|
||||||
|
child_docname: "items",
|
||||||
|
cannot_add_row: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (this.frm.doc.docstatus === 0) {
|
} else if (this.frm.doc.docstatus === 0) {
|
||||||
erpnext.set_unit_price_items_note(this.frm);
|
erpnext.set_unit_price_items_note(this.frm);
|
||||||
|
|
||||||
|
|||||||
@@ -345,3 +345,15 @@ def set_expired_status():
|
|||||||
""",
|
""",
|
||||||
(nowdate()),
|
(nowdate()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_purchased_items(supplier_quotation: str):
|
||||||
|
return frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
"Purchase Order Item",
|
||||||
|
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
|
||||||
|
fields=["supplier_quotation_item", "sum(qty)"],
|
||||||
|
group_by="supplier_quotation_item",
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,15 +2,115 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, today
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseOrder(FrappeTestCase):
|
class TestPurchaseOrder(FrappeTestCase):
|
||||||
|
def test_update_child_supplier_quotation_add_item(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": 5,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
sq.reload()
|
||||||
|
self.assertEqual(sq.get("items")[0].qty, 5)
|
||||||
|
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||||
|
|
||||||
|
def test_update_supplier_quotation_child_rate_disallow(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": 300,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_supplier_quotation_child_remove_item(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
po = make_purchase_order(sq.name)
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
po.get("items")[0].schedule_date = add_days(today(), 1)
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
po.submit()
|
||||||
|
sq.reload()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.savepoint("before_cancel")
|
||||||
|
# check if item having purchase order can be removed
|
||||||
|
self.assertRaises(
|
||||||
|
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||||
|
)
|
||||||
|
frappe.db.rollback(save_point="before_cancel")
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
sq.reload()
|
||||||
|
self.assertEqual(len(sq.get("items")), 1)
|
||||||
|
|
||||||
def test_supplier_quotation_qty(self):
|
def test_supplier_quotation_qty(self):
|
||||||
sq = frappe.copy_doc(test_records[0])
|
sq = frappe.copy_doc(test_records[0])
|
||||||
sq.items[0].qty = 0
|
sq.items[0].qty = 0
|
||||||
|
|||||||
@@ -3678,7 +3678,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||||
|
|
||||||
if child_doctype == "Purchase Order Item":
|
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||||
# Initialized value will update in parent validation
|
# Initialized value will update in parent validation
|
||||||
child_item.base_rate = 1
|
child_item.base_rate = 1
|
||||||
child_item.base_amount = 1
|
child_item.base_amount = 1
|
||||||
@@ -3696,7 +3696,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
return child_item
|
return child_item
|
||||||
|
|
||||||
|
|
||||||
def validate_child_on_delete(row, parent):
|
def validate_child_on_delete(row, parent, ordered_item=None):
|
||||||
"""Check if partially transacted item (row) is being deleted."""
|
"""Check if partially transacted item (row) is being deleted."""
|
||||||
if parent.doctype == "Sales Order":
|
if parent.doctype == "Sales Order":
|
||||||
if flt(row.delivered_qty):
|
if flt(row.delivered_qty):
|
||||||
@@ -3724,13 +3724,17 @@ def validate_child_on_delete(row, parent):
|
|||||||
row.idx, row.item_code
|
row.idx, row.item_code
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if parent.doctype in ["Purchase Order", "Sales Order"]:
|
||||||
if flt(row.billed_amt):
|
if flt(row.billed_amt):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||||
row.idx, row.item_code
|
row.idx, row.item_code
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
if parent.doctype == "Quotation":
|
||||||
|
if ordered_item.get(row.name):
|
||||||
|
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||||
|
|
||||||
|
|
||||||
def update_bin_on_delete(row, doctype):
|
def update_bin_on_delete(row, doctype):
|
||||||
@@ -3756,7 +3760,7 @@ def update_bin_on_delete(row, doctype):
|
|||||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||||
|
|
||||||
|
|
||||||
def validate_and_delete_children(parent, data) -> bool:
|
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||||
deleted_children = []
|
deleted_children = []
|
||||||
updated_item_names = [d.get("docname") for d in data]
|
updated_item_names = [d.get("docname") for d in data]
|
||||||
for item in parent.items:
|
for item in parent.items:
|
||||||
@@ -3764,7 +3768,7 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
deleted_children.append(item)
|
deleted_children.append(item)
|
||||||
|
|
||||||
for d in deleted_children:
|
for d in deleted_children:
|
||||||
validate_child_on_delete(d, parent)
|
validate_child_on_delete(d, parent, ordered_item)
|
||||||
d.cancel()
|
d.cancel()
|
||||||
d.delete()
|
d.delete()
|
||||||
|
|
||||||
@@ -3773,16 +3777,19 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
|
|
||||||
# need to update ordered qty in Material Request first
|
# need to update ordered qty in Material Request first
|
||||||
# bin uses Material Request Items to recalculate & update
|
# bin uses Material Request Items to recalculate & update
|
||||||
parent.update_prevdoc_status()
|
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
|
||||||
|
parent.update_prevdoc_status()
|
||||||
for d in deleted_children:
|
for d in deleted_children:
|
||||||
update_bin_on_delete(d, parent.doctype)
|
update_bin_on_delete(d, parent.doctype)
|
||||||
|
|
||||||
return bool(deleted_children)
|
return bool(deleted_children)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||||
|
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
|
||||||
|
|
||||||
def check_doc_permissions(doc, perm_type="create"):
|
def check_doc_permissions(doc, perm_type="create"):
|
||||||
try:
|
try:
|
||||||
doc.check_permission(perm_type)
|
doc.check_permission(perm_type)
|
||||||
@@ -3821,7 +3828,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_new_child_item(item_row):
|
def get_new_child_item(item_row):
|
||||||
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
|
child_doctype = parent_doctype + " Item"
|
||||||
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
||||||
|
|
||||||
def is_allowed_zero_qty():
|
def is_allowed_zero_qty():
|
||||||
@@ -3846,6 +3853,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
||||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||||
|
|
||||||
|
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||||
|
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||||
|
parent_doctype == "Supplier Quotation" and not purchased_items
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
qty_to_check = (
|
||||||
|
ordered_items.get(child_item.name)
|
||||||
|
if parent_doctype == "Quotation"
|
||||||
|
else purchased_items.get(child_item.name)
|
||||||
|
)
|
||||||
|
if qty_to_check:
|
||||||
|
if flt(new_data.get("qty")) < qty_to_check:
|
||||||
|
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||||
|
|
||||||
def should_update_supplied_items(doc) -> bool:
|
def should_update_supplied_items(doc) -> bool:
|
||||||
"""Subcontracted PO can allow following changes *after submit*:
|
"""Subcontracted PO can allow following changes *after submit*:
|
||||||
|
|
||||||
@@ -3888,7 +3910,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||||
|
|
||||||
data = json.loads(trans_items)
|
data = json.loads(trans_items)
|
||||||
|
|
||||||
any_qty_changed = False # updated to true if any item's qty changes
|
any_qty_changed = False # updated to true if any item's qty changes
|
||||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||||
any_conversion_factor_changed = False
|
any_conversion_factor_changed = False
|
||||||
@@ -3896,7 +3917,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||||
|
|
||||||
check_doc_permissions(parent, "write")
|
check_doc_permissions(parent, "write")
|
||||||
_removed_items = validate_and_delete_children(parent, data)
|
|
||||||
|
if parent_doctype == "Quotation":
|
||||||
|
ordered_items = get_ordered_items(parent.name)
|
||||||
|
_removed_items = validate_and_delete_children(parent, data, ordered_items)
|
||||||
|
elif parent_doctype == "Supplier Quotation":
|
||||||
|
purchased_items = get_purchased_items(parent.name)
|
||||||
|
_removed_items = validate_and_delete_children(parent, data, purchased_items)
|
||||||
|
else:
|
||||||
|
_removed_items = validate_and_delete_children(parent, data)
|
||||||
|
|
||||||
items_added_or_removed |= _removed_items
|
items_added_or_removed |= _removed_items
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
@@ -3936,7 +3966,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||||
date_unchanged = (
|
date_unchanged = (
|
||||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
(prev_date == getdate(new_date) if prev_date and new_date else False)
|
||||||
|
if parent_doctype not in ["Quotation", "Supplier Quotation"]
|
||||||
|
else None
|
||||||
) # in case of delivery note etc
|
) # in case of delivery note etc
|
||||||
if (
|
if (
|
||||||
rate_unchanged
|
rate_unchanged
|
||||||
@@ -3949,6 +3981,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
validate_quantity(child_item, d)
|
validate_quantity(child_item, d)
|
||||||
|
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||||
|
if not rate_unchanged:
|
||||||
|
frappe.throw(_("Rates cannot be modified for quoted items"))
|
||||||
|
|
||||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||||
any_qty_changed = True
|
any_qty_changed = True
|
||||||
|
|
||||||
@@ -3972,18 +4008,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
rate_unchanged = prev_rate == new_rate
|
rate_unchanged = prev_rate == new_rate
|
||||||
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
||||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||||
|
|
||||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||||
row_rate = flt(d.get("rate"), rate_precision)
|
row_rate = flt(d.get("rate"), rate_precision)
|
||||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
|
||||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||||
)
|
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||||
if amount_below_billed_amt and row_rate > 0.0:
|
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
|
||||||
).format(child_item.idx, child_item.item_code)
|
|
||||||
)
|
)
|
||||||
|
if amount_below_billed_amt and row_rate > 0.0:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||||
|
).format(child_item.idx, child_item.item_code)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
child_item.rate = row_rate
|
||||||
else:
|
else:
|
||||||
child_item.rate = row_rate
|
child_item.rate = row_rate
|
||||||
|
|
||||||
@@ -4017,26 +4056,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
if d.get("bom_no") and parent_doctype == "Sales Order":
|
if d.get("bom_no") and parent_doctype == "Sales Order":
|
||||||
child_item.bom_no = d.get("bom_no")
|
child_item.bom_no = d.get("bom_no")
|
||||||
|
|
||||||
if flt(child_item.price_list_rate):
|
if parent_doctype in ["Sales Order", "Purchase Order"]:
|
||||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
if flt(child_item.price_list_rate):
|
||||||
# if rate is greater than price_list_rate, set margin
|
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||||
# or set discount
|
# if rate is greater than price_list_rate, set margin
|
||||||
child_item.discount_percentage = 0
|
# or set discount
|
||||||
child_item.margin_type = "Amount"
|
child_item.discount_percentage = 0
|
||||||
child_item.margin_rate_or_amount = flt(
|
child_item.margin_type = "Amount"
|
||||||
child_item.rate - child_item.price_list_rate,
|
child_item.margin_rate_or_amount = flt(
|
||||||
child_item.precision("margin_rate_or_amount"),
|
child_item.rate - child_item.price_list_rate,
|
||||||
)
|
child_item.precision("margin_rate_or_amount"),
|
||||||
child_item.rate_with_margin = child_item.rate
|
)
|
||||||
else:
|
child_item.rate_with_margin = child_item.rate
|
||||||
child_item.discount_percentage = flt(
|
else:
|
||||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
child_item.discount_percentage = flt(
|
||||||
child_item.precision("discount_percentage"),
|
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||||
)
|
child_item.precision("discount_percentage"),
|
||||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
)
|
||||||
child_item.margin_type = ""
|
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||||
child_item.margin_rate_or_amount = 0
|
child_item.margin_type = ""
|
||||||
child_item.rate_with_margin = 0
|
child_item.margin_rate_or_amount = 0
|
||||||
|
child_item.rate_with_margin = 0
|
||||||
|
|
||||||
child_item.flags.ignore_validate_update_after_submit = True
|
child_item.flags.ignore_validate_update_after_submit = True
|
||||||
if new_child_flag:
|
if new_child_flag:
|
||||||
@@ -4058,14 +4098,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.doctype, parent.company, parent.base_grand_total
|
parent.doctype, parent.company, parent.base_grand_total
|
||||||
)
|
)
|
||||||
|
|
||||||
parent.set_payment_schedule()
|
if parent_doctype != "Supplier Quotation":
|
||||||
|
parent.set_payment_schedule()
|
||||||
if parent_doctype == "Purchase Order":
|
if parent_doctype == "Purchase Order":
|
||||||
parent.set_tax_withholding()
|
parent.set_tax_withholding()
|
||||||
parent.validate_minimum_order_qty()
|
parent.validate_minimum_order_qty()
|
||||||
parent.validate_budget()
|
parent.validate_budget()
|
||||||
if parent.is_against_so():
|
if parent.is_against_so():
|
||||||
parent.update_status_updater()
|
parent.update_status_updater()
|
||||||
else:
|
elif parent_doctype == "Sales Order":
|
||||||
parent.check_credit_limit()
|
parent.check_credit_limit()
|
||||||
|
|
||||||
# reset index of child table
|
# reset index of child table
|
||||||
@@ -4098,7 +4139,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||||
).format(frappe.bold(parent.name))
|
).format(frappe.bold(parent.name))
|
||||||
)
|
)
|
||||||
else: # Sales Order
|
elif parent_doctype == "Sales Order":
|
||||||
parent.validate_selling_price()
|
parent.validate_selling_price()
|
||||||
parent.validate_for_duplicate_items()
|
parent.validate_for_duplicate_items()
|
||||||
parent.validate_warehouse()
|
parent.validate_warehouse()
|
||||||
@@ -4110,9 +4151,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.reload()
|
parent.reload()
|
||||||
validate_workflow_conditions(parent)
|
validate_workflow_conditions(parent)
|
||||||
|
|
||||||
parent.update_blanket_order()
|
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||||
parent.update_billing_percentage()
|
parent.update_blanket_order()
|
||||||
parent.set_status()
|
parent.update_billing_percentage()
|
||||||
|
parent.set_status()
|
||||||
|
|
||||||
parent.validate_uom_is_integer("uom", "qty")
|
parent.validate_uom_is_integer("uom", "qty")
|
||||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
|
|||||||
@@ -123,6 +123,13 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
||||||
) {
|
) {
|
||||||
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
||||||
|
this.frm.add_custom_button(__("Update Items"), () => {
|
||||||
|
erpnext.utils.update_child_items({
|
||||||
|
frm: this.frm,
|
||||||
|
child_docname: "items",
|
||||||
|
cannot_add_row: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
||||||
|
|||||||
@@ -614,6 +614,7 @@ def handle_mandatory_error(e, customer, lead_name):
|
|||||||
frappe.throw(message, title=_("Mandatory Missing"))
|
frappe.throw(message, title=_("Mandatory Missing"))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_ordered_items(quotation: str):
|
def get_ordered_items(quotation: str):
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
frappe.get_all(
|
frappe.get_all(
|
||||||
|
|||||||
@@ -1,17 +1,114 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
test_dependencies = ["Product Bundle"]
|
test_dependencies = ["Product Bundle"]
|
||||||
|
|
||||||
|
|
||||||
class TestQuotation(FrappeTestCase):
|
class TestQuotation(FrappeTestCase):
|
||||||
|
def test_update_child_quotation_add_item(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item_1 = make_item("_Test Item")
|
||||||
|
item_2 = make_item("_Test Item 1")
|
||||||
|
|
||||||
|
item_list = [
|
||||||
|
{"item_code": item_1.item_code, "warehouse": "", "qty": 10, "rate": 300},
|
||||||
|
{"item_code": item_2.item_code, "warehouse": "", "qty": 5, "rate": 400},
|
||||||
|
]
|
||||||
|
|
||||||
|
qo = make_quotation(item_list=item_list)
|
||||||
|
first_item = qo.get("items")[0]
|
||||||
|
second_item = qo.get("items")[1]
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": first_item.item_code,
|
||||||
|
"rate": first_item.rate,
|
||||||
|
"qty": 11,
|
||||||
|
"docname": first_item.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": second_item.item_code,
|
||||||
|
"rate": second_item.rate,
|
||||||
|
"qty": second_item.qty,
|
||||||
|
"docname": second_item.name,
|
||||||
|
},
|
||||||
|
{"item_code": "_Test Item 2", "rate": 100, "qty": 7},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(qo.get("items")[0].qty, 11)
|
||||||
|
self.assertEqual(qo.get("items")[-1].rate, 100)
|
||||||
|
|
||||||
|
def test_update_child_disallow_rate_change(self):
|
||||||
|
qo = make_quotation(qty=4)
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": 5000,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||||
|
|
||||||
|
def test_update_child_removing_item(self):
|
||||||
|
qo = make_quotation(qty=10)
|
||||||
|
sales_order = make_sales_order(qo.name)
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": qo.items[0].rate,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
},
|
||||||
|
{"item_code": "_Test Item 2", "rate": 100, "qty": 7},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
sales_order.submit()
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(qo.status, "Partially Ordered")
|
||||||
|
|
||||||
|
trans_item = json.dumps([{"item_code": "_Test Item 2", "rate": 100, "qty": 7}])
|
||||||
|
|
||||||
|
# check if items having a sales order can be removed
|
||||||
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": qo.items[0].rate,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove item with no sales order
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(len(qo.get("items")), 1)
|
||||||
|
|
||||||
def test_quotation_qty(self):
|
def test_quotation_qty(self):
|
||||||
qo = make_quotation(qty=0, do_not_save=True)
|
qo = make_quotation(qty=0, do_not_save=True)
|
||||||
with self.assertRaises(InvalidQtyError):
|
with self.assertRaises(InvalidQtyError):
|
||||||
|
|||||||
Reference in New Issue
Block a user