Merge branch 'develop' into fix-pos-issues-again
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js
index 741e78f..7108cab 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.js
+++ b/erpnext/e_commerce/doctype/website_item/website_item.js
@@ -5,6 +5,12 @@
onload: function(frm) {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
+
+ frm.set_query("website_warehouse", () => {
+ return {
+ filters: {"is_group": 0}
+ };
+ });
},
image: function() {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index e8759f5..59ddf1f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -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',
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 2b6e696..349f40e 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -32,6 +32,7 @@
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 @@
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 = {d.name for d in self.po_items}
+
+ 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"""
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 6425374..ec49703 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -667,6 +667,39 @@
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
diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
index f829d57..df5862f 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -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": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 8d7ab85..e832ac9 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -457,7 +457,8 @@
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:
@@ -644,9 +645,13 @@
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)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a135484..ddb524c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -360,5 +360,5 @@
erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v14_0.update_employee_advance_status
erpnext.patches.v13_0.add_cost_center_in_loans
-erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
-erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
\ No newline at end of file
+erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
+erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py
index 57d78a2..75aa1d3 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -40,18 +40,15 @@
filters=filters,
order_by=sort_by + ' ' + sort_order,
limit_start=start,
- limit_page_length='21')
+ limit_page_length=21)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item in items:
item.update({
- 'item_name': frappe.get_cached_value(
- "Item", item.item_code, 'item_name'),
- 'disable_quick_entry': frappe.get_cached_value(
- "Item", item.item_code, 'has_batch_no')
- or frappe.get_cached_value(
- "Item", item.item_code, 'has_serial_no'),
+ 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
+ 'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no')
+ or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'),
'projected_qty': flt(item.projected_qty, precision),
'reserved_qty': flt(item.reserved_qty, precision),
'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 112420f..05e6e76 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -685,6 +685,12 @@
# standalone return
make_purchase_receipt(is_return=True, qty=-1, **typical_args)
+ def test_item_dashboard(self):
+ from erpnext.stock.dashboard.item_dashboard import get_data
+
+ self.assertTrue(get_data(item_code="_Test Item"))
+ self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
+ self.assertTrue(get_data(item_group="All Item Groups"))
def set_item_variant_settings(fields):
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index 4e8d71f..0ec812c 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -396,6 +396,21 @@
rule_1.delete()
rule_2.delete()
+ def test_warehouse_capacity_dashbord(self):
+ from erpnext.stock.dashboard.warehouse_capacity_dashboard import get_data
+
+ item = "_Rice"
+ rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500,
+ uom="Kg")
+
+ capacities = get_data(warehouse=self.warehouse_1)
+ for capacity in capacities:
+ if capacity.item_code == item and capacity.warehouse == self.warehouse_1:
+ self.assertEqual(capacity.stock_capacity, 500)
+
+ get_data(warehouse=self.warehouse_1)
+ rule.delete()
+
def create_putaway_rule(**args):
args = frappe._dict(args)
putaway = frappe.new_doc("Putaway Rule")
diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py
index b6fb636..5b82c7b 100644
--- a/erpnext/tests/test_zform_loads.py
+++ b/erpnext/tests/test_zform_loads.py
@@ -1,13 +1,14 @@
-""" dumb test to check all function calls on known form loads """
-
-import unittest
+""" smoak tests to check basic functionality calls on known form loads."""
import frappe
from frappe.desk.form.load import getdoc
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.www.printview import get_html_and_style
-class TestFormLoads(unittest.TestCase):
+class TestFormLoads(FrappeTestCase):
+ @change_settings("Print Settings", {"allow_print_for_cancelled": 1})
def test_load(self):
erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
@@ -17,14 +18,35 @@
if not last_doc:
continue
with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
- try:
- # reset previous response
- frappe.response = frappe._dict({"docs":[]})
- frappe.response.docinfo = None
+ self.assertFormLoad(doctype, last_doc)
+ self.assertDocPrint(doctype, last_doc)
- getdoc(doctype, last_doc)
- except Exception as e:
- self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
+ def assertFormLoad(self, doctype, docname):
+ # reset previous response
+ frappe.response = frappe._dict({"docs":[]})
+ frappe.response.docinfo = None
- self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
- self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
+ try:
+ getdoc(doctype, docname)
+ except Exception as e:
+ self.fail(f"Failed to load {doctype}-{docname}: {e}")
+
+ self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
+ self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
+
+ def assertDocPrint(self, doctype, docname):
+ doc = frappe.get_doc(doctype, docname)
+ doc.set("__onload", frappe._dict())
+ doc.run_method("onload")
+
+ messages_before = frappe.get_message_log()
+ ret = get_html_and_style(doc=doc.as_json(), print_format="Standard", no_letterhead=1)
+ messages_after = frappe.get_message_log()
+
+ if len(messages_after) > len(messages_before):
+ new_messages = messages_after[len(messages_before):]
+ self.fail("Print view showing error/warnings: \n"
+ + "\n".join(str(msg) for msg in new_messages))
+
+ # html should exist
+ self.assertTrue(bool(ret["html"]))