mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 20:59:11 +00:00
Merge branch 'develop' into fix-pos-issues-again
This commit is contained in:
@@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
|
|||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
// should never check Private
|
// should never check Private
|
||||||
frm.fields_dict["website_image"].df.is_private = 0;
|
frm.fields_dict["website_image"].df.is_private = 0;
|
||||||
|
|
||||||
|
frm.set_query("website_warehouse", () => {
|
||||||
|
return {
|
||||||
|
filters: {"is_group": 0}
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
image: function() {
|
image: function() {
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Production Plan', {
|
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) {
|
setup: function(frm) {
|
||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
'Work Order': 'Work Order / Subcontract PO',
|
'Work Order': 'Work Order / Subcontract PO',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ProductionPlan(Document):
|
|||||||
self.set_pending_qty_in_row_without_reference()
|
self.set_pending_qty_in_row_without_reference()
|
||||||
self.calculate_total_planned_qty()
|
self.calculate_total_planned_qty()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
self._rename_temporary_references()
|
||||||
|
|
||||||
def set_pending_qty_in_row_without_reference(self):
|
def set_pending_qty_in_row_without_reference(self):
|
||||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||||
@@ -57,6 +58,18 @@ class ProductionPlan(Document):
|
|||||||
if not flt(d.planned_qty):
|
if not flt(d.planned_qty):
|
||||||
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
|
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()
|
@frappe.whitelist()
|
||||||
def get_open_sales_orders(self):
|
def get_open_sales_orders(self):
|
||||||
""" Pull sales orders which are pending to deliver based on criteria selected"""
|
""" Pull sales orders which are pending to deliver based on criteria selected"""
|
||||||
|
|||||||
@@ -667,6 +667,39 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
wo_doc.submit()
|
wo_doc.submit()
|
||||||
self.assertEqual(wo_doc.qty, 0.55)
|
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):
|
def create_production_plan(**args):
|
||||||
"""
|
"""
|
||||||
sales_order (obj): Sales Order Doc Object
|
sales_order (obj): Sales Order Doc Object
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
"product_bundle_item",
|
"product_bundle_item",
|
||||||
"item_reference"
|
"item_reference",
|
||||||
|
"temporary_name"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -204,17 +205,25 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Item Reference"
|
"label": "Item Reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "temporary_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "temporary name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-28 18:31:06.822168",
|
"modified": "2022-03-24 04:54:09.940224",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Item",
|
"name": "Production Plan Item",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC"
|
"sort_order": "ASC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -457,7 +457,8 @@ class WorkOrder(Document):
|
|||||||
mr_obj.update_requested_qty([self.material_request_item])
|
mr_obj.update_requested_qty([self.material_request_item])
|
||||||
|
|
||||||
def update_ordered_qty(self):
|
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
|
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
@@ -644,9 +645,13 @@ class WorkOrder(Document):
|
|||||||
if not self.qty > 0:
|
if not self.qty > 0:
|
||||||
frappe.throw(_("Quantity to Manufacture must be greater than 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)
|
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",
|
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||||
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
|
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
|
||||||
|
|
||||||
|
|||||||
@@ -360,5 +360,5 @@ erpnext.patches.v14_0.update_batch_valuation_flag
|
|||||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||||
erpnext.patches.v14_0.update_employee_advance_status
|
erpnext.patches.v14_0.update_employee_advance_status
|
||||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
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
|
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
|
||||||
|
|||||||
@@ -40,18 +40,15 @@ def get_data(item_code=None, warehouse=None, item_group=None,
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
order_by=sort_by + ' ' + sort_order,
|
order_by=sort_by + ' ' + sort_order,
|
||||||
limit_start=start,
|
limit_start=start,
|
||||||
limit_page_length='21')
|
limit_page_length=21)
|
||||||
|
|
||||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item.update({
|
item.update({
|
||||||
'item_name': frappe.get_cached_value(
|
'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
|
||||||
"Item", item.item_code, 'item_name'),
|
'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no')
|
||||||
'disable_quick_entry': frappe.get_cached_value(
|
or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'),
|
||||||
"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),
|
'projected_qty': flt(item.projected_qty, precision),
|
||||||
'reserved_qty': flt(item.reserved_qty, precision),
|
'reserved_qty': flt(item.reserved_qty, precision),
|
||||||
'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
|
'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
|
||||||
|
|||||||
@@ -685,6 +685,12 @@ class TestItem(FrappeTestCase):
|
|||||||
# standalone return
|
# standalone return
|
||||||
make_purchase_receipt(is_return=True, qty=-1, **typical_args)
|
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):
|
def set_item_variant_settings(fields):
|
||||||
|
|||||||
@@ -396,6 +396,21 @@ class TestPutawayRule(FrappeTestCase):
|
|||||||
rule_1.delete()
|
rule_1.delete()
|
||||||
rule_2.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):
|
def create_putaway_rule(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
putaway = frappe.new_doc("Putaway Rule")
|
putaway = frappe.new_doc("Putaway Rule")
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
""" dumb test to check all function calls on known form loads """
|
""" smoak tests to check basic functionality calls on known form loads."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.desk.form.load import getdoc
|
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):
|
def test_load(self):
|
||||||
erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
|
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")
|
doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
|
||||||
@@ -17,14 +18,35 @@ class TestFormLoads(unittest.TestCase):
|
|||||||
if not last_doc:
|
if not last_doc:
|
||||||
continue
|
continue
|
||||||
with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
|
with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
|
||||||
try:
|
self.assertFormLoad(doctype, last_doc)
|
||||||
# reset previous response
|
self.assertDocPrint(doctype, last_doc)
|
||||||
frappe.response = frappe._dict({"docs":[]})
|
|
||||||
frappe.response.docinfo = None
|
|
||||||
|
|
||||||
getdoc(doctype, last_doc)
|
def assertFormLoad(self, doctype, docname):
|
||||||
except Exception as e:
|
# reset previous response
|
||||||
self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
|
frappe.response = frappe._dict({"docs":[]})
|
||||||
|
frappe.response.docinfo = None
|
||||||
|
|
||||||
self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
|
try:
|
||||||
self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
|
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"]))
|
||||||
|
|||||||
Reference in New Issue
Block a user