Merge pull request #36752 from s-aga-r/FR-35157
feat: `Update Items` for Subcontract Purchase Order
This commit is contained in:
commit
5b62bbe073
@ -185,8 +185,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
||||
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) {
|
||||
// Don't add Update Items button if the PO is following the new subcontracting flow.
|
||||
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
|
||||
this.frm.add_custom_button(__('Update Items'), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
|
@ -52,6 +52,7 @@ class PurchaseOrder(BuyingController):
|
||||
def onload(self):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
self.set_onload("can_update_items", self.can_update_items())
|
||||
|
||||
def validate(self):
|
||||
super(PurchaseOrder, self).validate()
|
||||
@ -450,6 +451,17 @@ class PurchaseOrder(BuyingController):
|
||||
else:
|
||||
self.db_set("per_received", 0, update_modified=False)
|
||||
|
||||
def can_update_items(self) -> bool:
|
||||
result = True
|
||||
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
if frappe.db.exists(
|
||||
"Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]}
|
||||
):
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||
"""get last purchase rate for an item"""
|
||||
|
@ -901,6 +901,71 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
|
||||
def test_update_items_for_subcontracting_purchase_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
|
||||
return update_child_qty_rate(
|
||||
po.doctype,
|
||||
trans_items,
|
||||
po.name,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 7",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA7",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
po = create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
update_items(po, qty=20)
|
||||
po.reload()
|
||||
|
||||
# Test - 1: Items should be updated as there is no Subcontracting Order against PO
|
||||
self.assertEqual(po.items[0].qty, 20)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 20)
|
||||
|
||||
sco = get_subcontracting_order(po_name=po.name, warehouse="_Test Warehouse - _TC")
|
||||
|
||||
# Test - 2: ValidationError should be raised as there is Subcontracting Order against PO
|
||||
self.assertRaises(frappe.ValidationError, update_items, po=po, qty=30)
|
||||
|
||||
sco.reload()
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
|
||||
update_items(po, qty=30)
|
||||
po.reload()
|
||||
|
||||
# Test - 3: Items should be updated as the Subcontracting Order is cancelled
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
@ -2858,6 +2858,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
return update_supplied_items
|
||||
|
||||
def validate_fg_item_for_subcontracting(new_data, is_new):
|
||||
if is_new:
|
||||
if not new_data.get("fg_item"):
|
||||
frappe.throw(
|
||||
_("Finished Good Item is not specified for service item {0}").format(new_data["item_code"])
|
||||
)
|
||||
else:
|
||||
is_sub_contracted_item, default_bom = frappe.db.get_value(
|
||||
"Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
|
||||
)
|
||||
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
|
||||
)
|
||||
elif not default_bom:
|
||||
frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))
|
||||
|
||||
if not new_data.get("fg_item_qty"):
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
@ -2889,6 +2910,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
|
||||
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
|
||||
prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty"))
|
||||
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(
|
||||
d.get("conversion_factor")
|
||||
)
|
||||
@ -2901,6 +2923,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
rate_unchanged = prev_rate == new_rate
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
fg_qty_unchanged = prev_fg_qty == new_fg_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
@ -2910,6 +2933,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if (
|
||||
rate_unchanged
|
||||
and qty_unchanged
|
||||
and fg_qty_unchanged
|
||||
and conversion_factor_unchanged
|
||||
and uom_unchanged
|
||||
and date_unchanged
|
||||
@ -2920,6 +2944,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
if (
|
||||
parent.doctype == "Purchase Order"
|
||||
and parent.is_subcontracted
|
||||
and not parent.is_old_subcontracting_flow
|
||||
):
|
||||
validate_fg_item_for_subcontracting(d, new_child_flag)
|
||||
child_item.fg_item_qty = flt(d["fg_item_qty"])
|
||||
|
||||
if new_child_flag:
|
||||
child_item.fg_item = d["fg_item"]
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
@ -3023,11 +3058,20 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
|
||||
if parent.is_subcontracted:
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
else:
|
||||
if not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
@ -1090,7 +1090,7 @@ def get_subcontracting_order(**args):
|
||||
po = frappe.get_doc("Purchase Order", args.get("po_name"))
|
||||
|
||||
if po.is_subcontracted:
|
||||
return create_subcontracting_order(po_name=po.name, **args)
|
||||
return create_subcontracting_order(**args)
|
||||
|
||||
if not args.service_items:
|
||||
service_items = [
|
||||
|
@ -579,7 +579,9 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"qty": d.qty,
|
||||
"rate": d.rate,
|
||||
"uom": d.uom
|
||||
"uom": d.uom,
|
||||
"fg_item": d.fg_item,
|
||||
"fg_item_qty": d.fg_item_qty,
|
||||
}
|
||||
});
|
||||
|
||||
@ -678,6 +680,37 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
})
|
||||
}
|
||||
|
||||
if (frm.doc.doctype == 'Purchase Order' && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
|
||||
fields.push({
|
||||
fieldtype:'Link',
|
||||
fieldname:'fg_item',
|
||||
options: 'Item',
|
||||
reqd: 1,
|
||||
in_list_view: 0,
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __('Finished Good Item'),
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1,
|
||||
'default_bom': ['!=', '']
|
||||
}
|
||||
}
|
||||
},
|
||||
}, {
|
||||
fieldtype:'Float',
|
||||
fieldname:'fg_item_qty',
|
||||
reqd: 1,
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 0,
|
||||
label: __('Finished Good Item Qty'),
|
||||
precision: get_precision('fg_item_qty')
|
||||
})
|
||||
}
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
size: "extra-large",
|
||||
|
Loading…
x
Reference in New Issue
Block a user