mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-15 09:53:09 +00:00
Merge pull request #57909 from frappe/mergify/bp/version-15-hotfix/pr-57876
fix: allow selecting a warehouse for new items in the update items dialog (backport #57876)
This commit is contained in:
@@ -211,6 +211,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
|
|
||||||
po.load_from_db()
|
po.load_from_db()
|
||||||
existing_ordered_qty = get_ordered_qty()
|
existing_ordered_qty = get_ordered_qty()
|
||||||
|
existing_ordered_qty_in_new_warehouse = get_ordered_qty(warehouse="_Test Warehouse 2 - _TC")
|
||||||
first_item_of_po = po.get("items")[0]
|
first_item_of_po = po.get("items")[0]
|
||||||
|
|
||||||
trans_item = json.dumps(
|
trans_item = json.dumps(
|
||||||
@@ -221,16 +222,64 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
"qty": first_item_of_po.qty,
|
"qty": first_item_of_po.qty,
|
||||||
"docname": first_item_of_po.name,
|
"docname": first_item_of_po.name,
|
||||||
},
|
},
|
||||||
{"item_code": "_Test Item", "rate": 200, "qty": 7},
|
{"item_code": "_Test Item", "rate": 200, "qty": 7, "warehouse": "_Test Warehouse 2 - _TC"},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
update_child_qty_rate("Purchase Order", trans_item, po.name)
|
update_child_qty_rate("Purchase Order", trans_item, po.name)
|
||||||
|
|
||||||
po.reload()
|
po.reload()
|
||||||
self.assertEqual(len(po.get("items")), 2)
|
self.assertEqual(len(po.get("items")), 2)
|
||||||
|
self.assertEqual(po.get("items")[-1].warehouse, "_Test Warehouse 2 - _TC")
|
||||||
self.assertEqual(po.status, "To Receive and Bill")
|
self.assertEqual(po.status, "To Receive and Bill")
|
||||||
# ordered qty should increase on row addition
|
# ordered qty should increase on row addition, in the warehouse passed for the new row
|
||||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
|
self.assertEqual(get_ordered_qty(), existing_ordered_qty)
|
||||||
|
self.assertEqual(
|
||||||
|
get_ordered_qty(warehouse="_Test Warehouse 2 - _TC"),
|
||||||
|
existing_ordered_qty_in_new_warehouse + 7,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_child_adding_new_item_without_any_default_warehouse(self):
|
||||||
|
stock_item = make_item("_Test PO Item Without Default Warehouse", {"is_stock_item": 1}).name
|
||||||
|
service_item = make_item("_Test PO Item Non Stock", {"is_stock_item": 0}).name
|
||||||
|
|
||||||
|
po = create_purchase_order(do_not_save=1)
|
||||||
|
po.save()
|
||||||
|
po.submit()
|
||||||
|
first_item_of_po = po.get("items")[0]
|
||||||
|
|
||||||
|
stock_settings_default = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||||
|
frappe.db.set_single_value("Stock Settings", "default_warehouse", None)
|
||||||
|
self.addCleanup(
|
||||||
|
frappe.db.set_single_value, "Stock Settings", "default_warehouse", stock_settings_default
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_trans_items(item_code):
|
||||||
|
return json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": first_item_of_po.item_code,
|
||||||
|
"rate": first_item_of_po.rate,
|
||||||
|
"qty": first_item_of_po.qty,
|
||||||
|
"docname": first_item_of_po.name,
|
||||||
|
},
|
||||||
|
{"item_code": item_code, "rate": 200, "qty": 7},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.ValidationError,
|
||||||
|
"Cannot find a default warehouse",
|
||||||
|
update_child_qty_rate,
|
||||||
|
"Purchase Order",
|
||||||
|
get_trans_items(stock_item),
|
||||||
|
po.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Purchase Order", get_trans_items(service_item), po.name)
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.get("items")[-1].item_code, service_item)
|
||||||
|
self.assertFalse(po.get("items")[-1].warehouse)
|
||||||
|
|
||||||
def test_update_child_removing_item(self):
|
def test_update_child_removing_item(self):
|
||||||
po = create_purchase_order(do_not_save=1)
|
po = create_purchase_order(do_not_save=1)
|
||||||
@@ -416,11 +465,13 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
"item_code": item,
|
"item_code": item,
|
||||||
"rate": 100,
|
"rate": 100,
|
||||||
"qty": 1,
|
"qty": 1,
|
||||||
|
"warehouse": po.items[0].warehouse,
|
||||||
}, # added item whose tax account head already exists in PO
|
}, # added item whose tax account head already exists in PO
|
||||||
{
|
{
|
||||||
"item_code": new_item_with_tax.name,
|
"item_code": new_item_with_tax.name,
|
||||||
"rate": 100,
|
"rate": 100,
|
||||||
"qty": 1,
|
"qty": 1,
|
||||||
|
"warehouse": po.items[0].warehouse,
|
||||||
}, # added item whose tax account head is missing in PO
|
}, # added item whose tax account head is missing in PO
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ from erpnext.stock.get_item_details import (
|
|||||||
get_item_tax_map,
|
get_item_tax_map,
|
||||||
get_item_warehouse,
|
get_item_warehouse,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.utils import (
|
||||||
|
is_group_warehouse,
|
||||||
|
validate_disabled_warehouse,
|
||||||
|
validate_warehouse_company,
|
||||||
|
)
|
||||||
from erpnext.utilities.regional import temporary_flag
|
from erpnext.utilities.regional import temporary_flag
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
|
||||||
@@ -3722,7 +3727,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
|
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
|
||||||
child_item.stock_uom = item.stock_uom
|
child_item.stock_uom = item.stock_uom
|
||||||
child_item.uom = trans_item.get("uom") or item.stock_uom
|
child_item.uom = trans_item.get("uom") or item.stock_uom
|
||||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
child_item.warehouse = get_new_child_item_warehouse(p_doc, item, trans_item, child_doctype)
|
||||||
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
|
||||||
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
||||||
@@ -3731,20 +3736,45 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
# 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
|
||||||
if child_doctype == "Sales Order Item":
|
|
||||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
|
||||||
if not child_item.warehouse:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings."
|
|
||||||
).format(frappe.bold(item.item_code))
|
|
||||||
)
|
|
||||||
|
|
||||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||||
add_taxes_from_tax_template(child_item, p_doc)
|
add_taxes_from_tax_template(child_item, p_doc)
|
||||||
return child_item
|
return child_item
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: str) -> str | None:
|
||||||
|
"""Return the warehouse picked in the Update Items dialog, else the configured default.
|
||||||
|
|
||||||
|
Validates whichever warehouse was resolved, since a submitted parent skips validate().
|
||||||
|
"""
|
||||||
|
warehouse = trans_item.get("warehouse") or get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||||
|
|
||||||
|
if not warehouse:
|
||||||
|
if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in Stock Settings."
|
||||||
|
).format(frappe.bold(item.item_code))
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
validate_warehouse_company(warehouse, p_doc.company)
|
||||||
|
validate_disabled_warehouse(warehouse)
|
||||||
|
is_group_warehouse(warehouse)
|
||||||
|
return warehouse
|
||||||
|
|
||||||
|
|
||||||
|
def is_warehouse_required_for_new_child_item(child_doctype: str, item, trans_item: dict) -> bool:
|
||||||
|
"""Sales Order always needs one; buying documents only for stock rows, as in validate_stock_item_warehouse."""
|
||||||
|
if child_doctype == "Sales Order Item":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"):
|
||||||
|
return bool(item.is_stock_item and flt(trans_item.get("qty")) and not item.delivered_by_supplier)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_child_on_delete(row, parent, ordered_item=None):
|
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":
|
||||||
|
|||||||
@@ -648,6 +648,7 @@ erpnext.utils.update_child_items = function (opts) {
|
|||||||
qty: d.qty,
|
qty: d.qty,
|
||||||
rate: d.rate,
|
rate: d.rate,
|
||||||
uom: d.uom,
|
uom: d.uom,
|
||||||
|
warehouse: d.warehouse,
|
||||||
fg_item: d.fg_item,
|
fg_item: d.fg_item,
|
||||||
fg_item_qty: d.fg_item_qty,
|
fg_item_qty: d.fg_item_qty,
|
||||||
};
|
};
|
||||||
@@ -727,8 +728,14 @@ erpnext.utils.update_child_items = function (opts) {
|
|||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
const { qty, price_list_rate: rate, uom, conversion_factor, bom_no } = r.message;
|
const {
|
||||||
|
qty,
|
||||||
|
price_list_rate: rate,
|
||||||
|
uom,
|
||||||
|
conversion_factor,
|
||||||
|
bom_no,
|
||||||
|
warehouse,
|
||||||
|
} = r.message;
|
||||||
const row = dialog.fields_dict.trans_items.df.data.find(
|
const row = dialog.fields_dict.trans_items.df.data.find(
|
||||||
(doc) => doc.idx == me.doc.idx
|
(doc) => doc.idx == me.doc.idx
|
||||||
);
|
);
|
||||||
@@ -739,6 +746,7 @@ erpnext.utils.update_child_items = function (opts) {
|
|||||||
qty: me.doc.qty || qty,
|
qty: me.doc.qty || qty,
|
||||||
rate: me.doc.rate || rate,
|
rate: me.doc.rate || rate,
|
||||||
bom_no: bom_no,
|
bom_no: bom_no,
|
||||||
|
warehouse: me.doc.docname ? me.doc.warehouse : warehouse,
|
||||||
});
|
});
|
||||||
dialog.fields_dict.trans_items.grid.refresh();
|
dialog.fields_dict.trans_items.grid.refresh();
|
||||||
}
|
}
|
||||||
@@ -812,6 +820,29 @@ erpnext.utils.update_child_items = function (opts) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const warehouse_df = child_meta.fields.find((f) => f.fieldname == "warehouse");
|
||||||
|
if (warehouse_df) {
|
||||||
|
fields.splice(3, 0, {
|
||||||
|
fieldtype: "Link",
|
||||||
|
fieldname: "warehouse",
|
||||||
|
options: "Warehouse",
|
||||||
|
in_list_view: 1,
|
||||||
|
label: __(warehouse_df.label),
|
||||||
|
// only new rows may set it, existing rows would leave their
|
||||||
|
// reserved qty stranded in the previous warehouse's bin
|
||||||
|
read_only_depends_on: "eval:doc.docname",
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
is_group: 0,
|
||||||
|
disabled: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
frm.doc.doctype == "Purchase Order" &&
|
frm.doc.doctype == "Purchase Order" &&
|
||||||
frm.doc.is_subcontracted &&
|
frm.doc.is_subcontracted &&
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
|||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
from erpnext.stock.get_item_details import get_bin_details
|
from erpnext.stock.get_item_details import get_bin_details
|
||||||
|
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||||
|
|
||||||
|
|
||||||
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||||
@@ -558,6 +559,117 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
self.assertEqual(updated_total, prev_total + 1400)
|
self.assertEqual(updated_total, prev_total + 1400)
|
||||||
self.assertNotEqual(updated_total_in_words, prev_total_in_words)
|
self.assertNotEqual(updated_total_in_words, prev_total_in_words)
|
||||||
|
|
||||||
|
def test_update_child_adding_new_item_with_warehouse(self):
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=4)
|
||||||
|
|
||||||
|
first_item_of_so = so.get("items")[0]
|
||||||
|
self.assertNotEqual(first_item_of_so.warehouse, "_Test Warehouse 2 - _TC")
|
||||||
|
|
||||||
|
def get_trans_item(warehouse):
|
||||||
|
return json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": first_item_of_so.item_code,
|
||||||
|
"rate": first_item_of_so.rate,
|
||||||
|
"qty": first_item_of_so.qty,
|
||||||
|
"docname": first_item_of_so.name,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
},
|
||||||
|
{"item_code": "_Test Item 2", "rate": 200, "qty": 7, "warehouse": warehouse},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
InvalidWarehouseCompany,
|
||||||
|
update_child_qty_rate,
|
||||||
|
"Sales Order",
|
||||||
|
get_trans_item("_Test Warehouse 2 - _TC1"),
|
||||||
|
so.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.ValidationError,
|
||||||
|
"Group node warehouse",
|
||||||
|
update_child_qty_rate,
|
||||||
|
"Sales Order",
|
||||||
|
get_trans_item("_Test Warehouse Group - _TC"),
|
||||||
|
so.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Warehouse", "_Test Disabled Warehouse - _TC"):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Warehouse",
|
||||||
|
"warehouse_name": "_Test Disabled Warehouse",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"disabled": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.ValidationError,
|
||||||
|
"Disabled Warehouse",
|
||||||
|
update_child_qty_rate,
|
||||||
|
"Sales Order",
|
||||||
|
get_trans_item("_Test Disabled Warehouse - _TC"),
|
||||||
|
so.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Sales Order", get_trans_item("_Test Warehouse 2 - _TC"), so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
# the new row picks up the warehouse selected in the dialog
|
||||||
|
self.assertEqual(so.get("items")[-1].item_code, "_Test Item 2")
|
||||||
|
self.assertEqual(so.get("items")[-1].warehouse, "_Test Warehouse 2 - _TC")
|
||||||
|
# existing rows keep theirs, so their reserved qty stays in the same bin
|
||||||
|
self.assertEqual(so.get("items")[0].warehouse, first_item_of_so.warehouse)
|
||||||
|
|
||||||
|
def test_update_child_adding_new_item_without_any_default_warehouse(self):
|
||||||
|
item_code = make_item("_Test Item Without Default Warehouse", {"is_stock_item": 1}).name
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=4)
|
||||||
|
existing_item = so.get("items")[0]
|
||||||
|
|
||||||
|
stock_settings_default = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||||
|
frappe.db.set_single_value("Stock Settings", "default_warehouse", None)
|
||||||
|
self.addCleanup(
|
||||||
|
frappe.db.set_single_value, "Stock Settings", "default_warehouse", stock_settings_default
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_trans_items(warehouse=None):
|
||||||
|
new_row = {"item_code": item_code, "rate": 200, "qty": 7}
|
||||||
|
if warehouse:
|
||||||
|
new_row["warehouse"] = warehouse
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": existing_item.item_code,
|
||||||
|
"rate": existing_item.rate,
|
||||||
|
"qty": existing_item.qty,
|
||||||
|
"docname": existing_item.name,
|
||||||
|
},
|
||||||
|
new_row,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# no default in the Item Master, Item Group, Brand or Stock Settings
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.ValidationError,
|
||||||
|
"Cannot find a default warehouse",
|
||||||
|
update_child_qty_rate,
|
||||||
|
"Sales Order",
|
||||||
|
get_trans_items(),
|
||||||
|
so.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Sales Order", get_trans_items("_Test Warehouse - _TC"), so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(len(so.get("items")), 2)
|
||||||
|
self.assertEqual(so.get("items")[0].warehouse, existing_item.warehouse)
|
||||||
|
self.assertEqual(so.get("items")[-1].item_code, item_code)
|
||||||
|
self.assertEqual(so.get("items")[-1].warehouse, "_Test Warehouse - _TC")
|
||||||
|
|
||||||
def test_update_child_removing_item(self):
|
def test_update_child_removing_item(self):
|
||||||
so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]})
|
so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]})
|
||||||
create_dn_against_so(so.name, 2)
|
create_dn_against_so(so.name, 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user