fix: broken production item links on production plan (backport #30399) (#30400)

* fix: only validate qty for main non-subassy items

(cherry picked from commit 3d43c437ad)

* fix: subassembly items linked to temporary name

Production Plan tables for po_items and sub_assembly_items are prepared
client side so both dont exist at time of first save or modifying and
hence any "links" created are invalid. This change retains temporary
name so it can be relinked server side after naming is performed.

Co-Authored-By: Marica <maricadsouza221197@gmail.com>
(cherry picked from commit 5b1d6055e6)

* test: dont resubmit WO

[skip ci]

Co-authored-by: Ankush Menat <ankush@frappe.io>
This commit is contained in:
mergify[bot]
2022-03-24 17:30:58 +05:30
committed by GitHub
parent 26fe2388e1
commit 03d7fc1570
7 changed files with 73 additions and 7 deletions

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
before_save: function(frm) {
// preserve temporary names on production plan item to re-link sub-assembly items
frm.doc.po_items.forEach(item => {
item.temporary_name = item.name;
});
},
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',

View File

@@ -32,6 +32,7 @@ class ProductionPlan(Document):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
self.set_status()
self._rename_temporary_references()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@@ -57,6 +58,18 @@ class ProductionPlan(Document):
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
def _rename_temporary_references(self):
""" po_items and sub_assembly_items items are both constructed client side without saving.
Attempt to fix linkages by using temporary names to map final row names.
"""
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
actual_names = set(new_name_map.values())
for sub_assy in self.sub_assembly_items:
if sub_assy.production_plan_item not in actual_names:
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
@frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""

View File

@@ -617,6 +617,39 @@ class TestProductionPlan(FrappeTestCase):
wo_doc.submit()
self.assertEqual(wo_doc.qty, 0.55)
def test_temporary_name_relinking(self):
pp = frappe.new_doc("Production Plan")
# this can not be unittested so mocking data that would be expected
# from client side.
for _ in range(10):
po_item = pp.append("po_items", {
"name": frappe.generate_hash(length=10),
"temporary_name": frappe.generate_hash(length=10),
})
pp.append("sub_assembly_items", {
"production_plan_item": po_item.temporary_name
})
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
# bad links should be erased
pp.append("sub_assembly_items", {
"production_plan_item": frappe.generate_hash(length=16)
})
pp._rename_temporary_references()
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
pp.sub_assembly_items.pop()
# reattempting on same doc shouldn't change anything
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
def create_production_plan(**args):
"""
sales_order (obj): Sales Order Doc Object

View File

@@ -27,7 +27,8 @@
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
"item_reference",
"temporary_name"
],
"fields": [
{
@@ -204,17 +205,25 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Item Reference"
},
{
"fieldname": "temporary_name",
"fieldtype": "Data",
"hidden": 1,
"label": "temporary name"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 18:31:06.822168",
"modified": "2022-03-24 04:54:09.940224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -352,7 +352,6 @@ class TestWorkOrder(FrappeTestCase):
wo_order = make_wo_order_test_record(planned_start_date=now(),
sales_order=so.name, qty=3)
wo_order.submit()
self.assertEqual(wo_order.docstatus, 1)
allow_overproduction("overproduction_percentage_for_sales_order", 0)

View File

@@ -457,7 +457,8 @@ class WorkOrder(Document):
mr_obj.update_requested_qty([self.material_request_item])
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
if self.docstatus == 1:
@@ -640,9 +641,13 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
if not qty_dict:
return
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)

View File

@@ -352,6 +352,6 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.amazon_mws_deprecation_warning
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.rename_non_profit_fields
erpnext.patches.v13_0.enable_ksa_vat_docs #1