Merge pull request #36752 from s-aga-r/FR-35157

feat: `Update Items` for Subcontract Purchase Order
This commit is contained in:
s-aga-r 2023-08-23 11:13:28 +05:30 committed by GitHub
commit 5b62bbe073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 9 deletions

View File

@ -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,

View File

@ -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"""

View File

@ -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

View File

@ -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()

View File

@ -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 = [

View File

@ -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",