test: Util to update cost in all BOMs

- Utility to update cost in all BOMs without cron jobs or background jobs (run immediately)
- Re-use util wherever all bom costs are to be updated
- Skip explicit commits if in test
- Specify company in test records (dirty data sometimes, company wh mismatch)
- Skip background jobs queueing if in test
This commit is contained in:
marination 2022-06-07 14:44:00 +05:30
parent 15101190a6
commit 6bde1bb5d2
6 changed files with 97 additions and 52 deletions

View File

@ -11,7 +11,9 @@ from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
)
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_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
@ -80,7 +82,7 @@ class TestBOM(FrappeTestCase):
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10) reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
# update cost of all BOMs based on latest valuation rate # update cost of all BOMs based on latest valuation rate
update_cost() update_cost_in_all_boms_in_test()
# check if new valuation rate updated in all BOMs # check if new valuation rate updated in all BOMs
for d in frappe.db.sql( for d in frappe.db.sql(

View File

@ -32,6 +32,7 @@
"is_active": 1, "is_active": 1,
"is_default": 1, "is_default": 1,
"item": "_Test Item Home Desktop Manufactured", "item": "_Test Item Home Desktop Manufactured",
"company": "_Test Company",
"quantity": 1.0 "quantity": 1.0
}, },
{ {

View File

@ -1,7 +1,7 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json import json
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple, Union
import frappe import frappe
from frappe import _ from frappe import _
@ -101,12 +101,14 @@ def run_replace_bom_job(
handle_exception(doc) handle_exception(doc)
finally: finally:
frappe.db.auto_commit_on_many_writes = 0 frappe.db.auto_commit_on_many_writes = 0
frappe.db.commit() # nosemgrep
if not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def process_boms_cost_level_wise( def process_boms_cost_level_wise(
update_doc: "BOMUpdateLog", parent_boms: List[str] = None update_doc: "BOMUpdateLog", parent_boms: List[str] = None
) -> None: ) -> Union[None, Tuple]:
"Queue jobs at the start of new BOM Level in 'Update Cost' Jobs." "Queue jobs at the start of new BOM Level in 'Update Cost' Jobs."
current_boms = {} current_boms = {}
@ -133,6 +135,10 @@ def process_boms_cost_level_wise(
values = {"current_level": current_level} values = {"current_level": current_level}
set_values_in_log(update_doc.name, values, commit=True) set_values_in_log(update_doc.name, values, commit=True)
if frappe.flags.in_test:
return current_boms, current_level
queue_bom_cost_jobs(current_boms, update_doc, current_level) queue_bom_cost_jobs(current_boms, update_doc, current_level)
@ -155,6 +161,10 @@ def queue_bom_cost_jobs(
) )
batch_row.db_insert() batch_row.db_insert()
if frappe.flags.in_test:
# skip background jobs in test
return boms_to_process, batch_row.name
frappe.enqueue( frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level",
doc=update_doc, doc=update_doc,
@ -216,7 +226,10 @@ def resume_bom_cost_update_jobs():
def get_processed_current_boms( def get_processed_current_boms(
log: Dict[str, Any], bom_batches: Dict[str, Any] log: Dict[str, Any], bom_batches: Dict[str, Any]
) -> Tuple[List[str], Dict[str, Any]]: ) -> Tuple[List[str], Dict[str, Any]]:
"Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field and into current boms list." """
Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field
and into current boms list.
"""
processed_boms = json.loads(log.processed_boms) if log.processed_boms else {} processed_boms = json.loads(log.processed_boms) if log.processed_boms else {}
current_boms = [] current_boms = []

View File

@ -63,7 +63,9 @@ def update_cost_in_level(
handle_exception(doc) handle_exception(doc)
finally: finally:
frappe.db.auto_commit_on_many_writes = 0 frappe.db.auto_commit_on_many_writes = 0
frappe.db.commit() # nosemgrep
if not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
@ -119,8 +121,8 @@ def update_cost_in_boms(bom_list: List[str]) -> None:
bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
bom_doc.db_update() bom_doc.db_update()
if index % 100 == 0: if (index % 100 == 0) and not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit() # nosemgrep
def get_next_higher_level_boms( def get_next_higher_level_boms(
@ -210,7 +212,7 @@ def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = Fals
query = query.set(key, value) query = query.set(key, value)
query.run() query.run()
if commit: if commit and not frappe.flags.in_test:
frappe.db.commit() # nosemgrep frappe.db.commit() # nosemgrep

View File

@ -1,14 +1,27 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import json
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
BOMMissingError, BOMMissingError,
get_processed_current_boms,
process_boms_cost_level_wise,
queue_bom_cost_jobs,
run_replace_bom_job, run_replace_bom_job,
) )
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
get_next_higher_level_boms,
set_values_in_log,
update_cost_in_level,
)
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import (
enqueue_replace_bom,
enqueue_update_cost,
)
test_records = frappe.get_test_records("BOM") test_records = frappe.get_test_records("BOM")
@ -31,17 +44,12 @@ class TestBOMUpdateLog(FrappeTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
if self._testMethodName == "test_bom_update_log_completion":
# clear logs and delete BOM created via setUp
frappe.db.delete("BOM Update Log")
self.new_bom_doc.cancel()
self.new_bom_doc.delete()
# explicitly commit and restore to original state
frappe.db.commit() # nosemgrep
def test_bom_update_log_validate(self): def test_bom_update_log_validate(self):
"Test if BOM presence is validated." """
1) Test if BOM presence is validated.
2) Test if same BOMs are validated.
3) Test of non-existent BOM is validated.
"""
with self.assertRaises(BOMMissingError): with self.assertRaises(BOMMissingError):
enqueue_replace_bom(boms={}) enqueue_replace_bom(boms={})
@ -55,9 +63,7 @@ class TestBOMUpdateLog(FrappeTestCase):
def test_bom_update_log_queueing(self): def test_bom_update_log_queueing(self):
"Test if BOM Update Log is created and queued." "Test if BOM Update Log is created and queued."
log = enqueue_replace_bom( log = enqueue_replace_bom(boms=self.boms)
boms=self.boms,
)
self.assertEqual(log.docstatus, 1) self.assertEqual(log.docstatus, 1)
self.assertEqual(log.status, "Queued") self.assertEqual(log.status, "Queued")
@ -65,32 +71,51 @@ class TestBOMUpdateLog(FrappeTestCase):
def test_bom_update_log_completion(self): def test_bom_update_log_completion(self):
"Test if BOM Update Log handles job completion correctly." "Test if BOM Update Log handles job completion correctly."
log = enqueue_replace_bom( log = enqueue_replace_bom(boms=self.boms)
boms=self.boms,
)
# Explicitly commits log, new bom (setUp) and replacement impact. # Is run via background job IRL
# Is run via background jobs IRL run_replace_bom_job(doc=log, boms=self.boms)
run_replace_bom_job(
doc=log,
boms=self.boms,
update_type="Replace BOM",
)
log.reload() log.reload()
self.assertEqual(log.status, "Completed") self.assertEqual(log.status, "Completed")
# teardown (undo replace impact) due to commit
boms = frappe._dict( def update_cost_in_all_boms_in_test():
current_bom=self.boms.new_bom, """
new_bom=self.boms.current_bom, Utility to run 'Update Cost' job in tests immediately without Cron job.
Run job for all levels (manually) until fully complete.
"""
parent_boms = []
log = enqueue_update_cost() # create BOM Update Log
while log.status != "Completed":
level_boms, current_level = process_boms_cost_level_wise(log, parent_boms)
log.reload()
boms, batch = queue_bom_cost_jobs(
level_boms, log, current_level
) # adds rows in log for tracking
log.reload()
update_cost_in_level(log, boms, batch) # business logic
log.reload()
# current level done, get next level boms
bom_batches = frappe.db.get_all(
"BOM Update Batch",
{"parent": log.name, "level": log.current_level},
["name", "boms_updated", "status"],
) )
log2 = enqueue_replace_bom( current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
boms=self.boms, parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
set_values_in_log(
log.name,
values={
"processed_boms": json.dumps(processed_boms),
"status": "Completed" if not parent_boms else "In Progress",
},
) )
run_replace_bom_job( # Explicitly commits log.reload()
doc=log2,
boms=boms, return log
update_type="Replace BOM",
)
self.assertEqual(log2.status, "Completed")

View File

@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, 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 frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
)
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@ -25,8 +27,8 @@ class TestBOMUpdateTool(FrappeTestCase):
boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
replace_bom(boms) replace_bom(boms)
self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name)) self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
# reverse, as it affects other testcases # reverse, as it affects other testcases
boms.current_bom = bom_doc.name boms.current_bom = bom_doc.name
@ -52,13 +54,13 @@ class TestBOMUpdateTool(FrappeTestCase):
self.assertEqual(doc.total_cost, 200) self.assertEqual(doc.total_cost, 200)
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200) frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
update_cost() update_cost_in_all_boms_in_test()
doc.load_from_db() doc.load_from_db()
self.assertEqual(doc.total_cost, 300) self.assertEqual(doc.total_cost, 300)
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100) frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
update_cost() update_cost_in_all_boms_in_test()
doc.load_from_db() doc.load_from_db()
self.assertEqual(doc.total_cost, 200) self.assertEqual(doc.total_cost, 200)