Merge branch 'version-13-hotfix' into item-tax-templates
This commit is contained in:
		
						commit
						c99eaf106e
					
				
							
								
								
									
										12
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | # Since version 2.23 (released in August 2019), git-blame has a feature | ||||||
|  | # to ignore or bypass certain commits. | ||||||
|  | # | ||||||
|  | # This file contains a list of commits that are not likely what you | ||||||
|  | # are looking for in a blame, such as mass reformatting or renaming. | ||||||
|  | # You can set this file as a default ignore file for blame by running | ||||||
|  | # the following command. | ||||||
|  | # | ||||||
|  | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs | ||||||
|  | 
 | ||||||
|  | # This commit just changes spaces to tabs for indentation in some files | ||||||
|  | 5f473611bd6ed57703716244a054d3fb5ba9cd23 | ||||||
| @ -225,7 +225,7 @@ class AccountsController(TransactionBase): | |||||||
| 
 | 
 | ||||||
| 	def validate_date_with_fiscal_year(self): | 	def validate_date_with_fiscal_year(self): | ||||||
| 		if self.meta.get_field("fiscal_year"): | 		if self.meta.get_field("fiscal_year"): | ||||||
| 			date_field = "" | 			date_field = None | ||||||
| 			if self.meta.get_field("posting_date"): | 			if self.meta.get_field("posting_date"): | ||||||
| 				date_field = "posting_date" | 				date_field = "posting_date" | ||||||
| 			elif self.meta.get_field("transaction_date"): | 			elif self.meta.get_field("transaction_date"): | ||||||
| @ -1449,6 +1449,7 @@ def validate_and_delete_children(parent, data): | |||||||
| 	for d in deleted_children: | 	for d in deleted_children: | ||||||
| 		update_bin_on_delete(d, parent.doctype) | 		update_bin_on_delete(d, parent.doctype) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): | def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): | ||||||
| 	def check_doc_permissions(doc, perm_type='create'): | 	def check_doc_permissions(doc, perm_type='create'): | ||||||
|  | |||||||
| @ -1,17 +1,21 @@ | |||||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||||
| # License: GNU General Public License v3. See license.txt | # License: GNU General Public License v3. See license.txt | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals | import json | ||||||
| import frappe, erpnext |  | ||||||
| from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate |  | ||||||
| from frappe import _ |  | ||||||
| import frappe.defaults |  | ||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
| from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced | 
 | ||||||
|  | import frappe | ||||||
|  | import frappe.defaults | ||||||
|  | from frappe import _ | ||||||
|  | from frappe.utils import cint, cstr, flt, get_link_to_form, getdate | ||||||
|  | 
 | ||||||
|  | import erpnext | ||||||
| from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map | from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map | ||||||
|  | from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year | ||||||
| from erpnext.controllers.accounts_controller import AccountsController | from erpnext.controllers.accounts_controller import AccountsController | ||||||
| from erpnext.stock.stock_ledger import get_valuation_rate |  | ||||||
| from erpnext.stock import get_warehouse_account_map | from erpnext.stock import get_warehouse_account_map | ||||||
|  | from erpnext.stock.stock_ledger import get_valuation_rate | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class QualityInspectionRequiredError(frappe.ValidationError): pass | class QualityInspectionRequiredError(frappe.ValidationError): pass | ||||||
| class QualityInspectionRejectedError(frappe.ValidationError): pass | class QualityInspectionRejectedError(frappe.ValidationError): pass | ||||||
| @ -189,7 +193,6 @@ class StockController(AccountsController): | |||||||
| 		if hasattr(self, "items"): | 		if hasattr(self, "items"): | ||||||
| 			item_doclist = self.get("items") | 			item_doclist = self.get("items") | ||||||
| 		elif self.doctype == "Stock Reconciliation": | 		elif self.doctype == "Stock Reconciliation": | ||||||
| 			import json |  | ||||||
| 			item_doclist = [] | 			item_doclist = [] | ||||||
| 			data = json.loads(self.reconciliation_json) | 			data = json.loads(self.reconciliation_json) | ||||||
| 			for row in data[data.index(self.head_row)+1:]: | 			for row in data[data.index(self.head_row)+1:]: | ||||||
| @ -319,7 +322,7 @@ class StockController(AccountsController): | |||||||
| 		return serialized_items | 		return serialized_items | ||||||
| 
 | 
 | ||||||
| 	def validate_warehouse(self): | 	def validate_warehouse(self): | ||||||
| 		from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse | 		from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company | ||||||
| 
 | 
 | ||||||
| 		warehouses = list(set([d.warehouse for d in | 		warehouses = list(set([d.warehouse for d in | ||||||
| 			self.get("items") if getattr(d, "warehouse", None)])) | 			self.get("items") if getattr(d, "warehouse", None)])) | ||||||
| @ -498,6 +501,39 @@ class StockController(AccountsController): | |||||||
| 			check_if_stock_and_account_balance_synced(self.posting_date, | 			check_if_stock_and_account_balance_synced(self.posting_date, | ||||||
| 				self.company, self.doctype, self.name) | 				self.company, self.doctype, self.name) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | @frappe.whitelist() | ||||||
|  | def make_quality_inspections(doctype, docname, items): | ||||||
|  | 	if isinstance(items, str): | ||||||
|  | 		items = json.loads(items) | ||||||
|  | 
 | ||||||
|  | 	inspections = [] | ||||||
|  | 	for item in items: | ||||||
|  | 		if flt(item.get("sample_size")) > flt(item.get("qty")): | ||||||
|  | 			frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format( | ||||||
|  | 				item_name=item.get("item_name"), | ||||||
|  | 				sample_size=item.get("sample_size"), | ||||||
|  | 				accepted_quantity=item.get("qty") | ||||||
|  | 			)) | ||||||
|  | 
 | ||||||
|  | 		quality_inspection = frappe.get_doc({ | ||||||
|  | 			"doctype": "Quality Inspection", | ||||||
|  | 			"inspection_type": "Incoming", | ||||||
|  | 			"inspected_by": frappe.session.user, | ||||||
|  | 			"reference_type": doctype, | ||||||
|  | 			"reference_name": docname, | ||||||
|  | 			"item_code": item.get("item_code"), | ||||||
|  | 			"description": item.get("description"), | ||||||
|  | 			"sample_size": flt(item.get("sample_size")), | ||||||
|  | 			"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None, | ||||||
|  | 			"batch_no": item.get("batch_no") | ||||||
|  | 		}).insert() | ||||||
|  | 		quality_inspection.save() | ||||||
|  | 		inspections.append(quality_inspection.name) | ||||||
|  | 
 | ||||||
|  | 	return inspections | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def is_reposting_pending(): | def is_reposting_pending(): | ||||||
| 	return frappe.db.exists("Repost Item Valuation", | 	return frappe.db.exists("Repost Item Valuation", | ||||||
| 		{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) | 		{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) | ||||||
|  | |||||||
| @ -211,16 +211,27 @@ frappe.ui.form.on('Production Plan', { | |||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	get_items: function(frm) { | 	get_items: function (frm) { | ||||||
|  | 		frm.clear_table('prod_plan_references'); | ||||||
|  | 
 | ||||||
| 		frappe.call({ | 		frappe.call({ | ||||||
| 			method: "get_items", | 			method: "get_items", | ||||||
| 			freeze: true, | 			freeze: true, | ||||||
| 			doc: frm.doc, | 			doc: frm.doc, | ||||||
| 			callback: function() { | 			callback: function () { | ||||||
| 				refresh_field('po_items'); | 				refresh_field('po_items'); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
|  | 	combine_items: function (frm) { | ||||||
|  | 		frm.clear_table('prod_plan_references'); | ||||||
|  | 
 | ||||||
|  | 		frappe.call({ | ||||||
|  | 			method: "get_items", | ||||||
|  | 			freeze: true, | ||||||
|  | 			doc: frm.doc, | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
| 
 | 
 | ||||||
| 	get_items_for_mr: function(frm) { | 	get_items_for_mr: function(frm) { | ||||||
| 		if (!frm.doc.for_warehouse) { | 		if (!frm.doc.for_warehouse) { | ||||||
|  | |||||||
| @ -28,7 +28,10 @@ | |||||||
|   "material_requests", |   "material_requests", | ||||||
|   "select_items_to_manufacture_section", |   "select_items_to_manufacture_section", | ||||||
|   "get_items", |   "get_items", | ||||||
|  |   "combine_items", | ||||||
|   "po_items", |   "po_items", | ||||||
|  |   "section_break_25", | ||||||
|  |   "prod_plan_references", | ||||||
|   "material_request_planning", |   "material_request_planning", | ||||||
|   "include_non_stock_items", |   "include_non_stock_items", | ||||||
|   "include_subcontracted_items", |   "include_subcontracted_items", | ||||||
| @ -316,13 +319,31 @@ | |||||||
|    "fieldname": "include_safety_stock", |    "fieldname": "include_safety_stock", | ||||||
|    "fieldtype": "Check", |    "fieldtype": "Check", | ||||||
|    "label": "Include Safety Stock in Required Qty Calculation" |    "label": "Include Safety Stock in Required Qty Calculation" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "default": "0", | ||||||
|  |    "depends_on": "eval:doc.get_items_from == 'Sales Order'", | ||||||
|  |    "fieldname": "combine_items", | ||||||
|  |    "fieldtype": "Check", | ||||||
|  |    "label": "Consolidate Items" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "section_break_25", | ||||||
|  |    "fieldtype": "Section Break" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "prod_plan_references", | ||||||
|  |    "fieldtype": "Table", | ||||||
|  |    "hidden": 1, | ||||||
|  |    "label": "Production Plan Item Reference", | ||||||
|  |    "options": "Production Plan Item Reference" | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "icon": "fa fa-calendar", |  "icon": "fa fa-calendar", | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-08 11:17:25.470147", |  "modified": "2021-05-24 16:59:03.643211", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Manufacturing", |  "module": "Manufacturing", | ||||||
|  "name": "Production Plan", |  "name": "Production Plan", | ||||||
|  | |||||||
| @ -96,8 +96,10 @@ class ProductionPlan(Document): | |||||||
| 
 | 
 | ||||||
| 	@frappe.whitelist() | 	@frappe.whitelist() | ||||||
| 	def get_items(self): | 	def get_items(self): | ||||||
|  | 		self.set('po_items', []) | ||||||
| 		if self.get_items_from == "Sales Order": | 		if self.get_items_from == "Sales Order": | ||||||
| 			self.get_so_items() | 			self.get_so_items()	 | ||||||
|  | 
 | ||||||
| 		elif self.get_items_from == "Material Request": | 		elif self.get_items_from == "Material Request": | ||||||
| 			self.get_mr_items() | 			self.get_mr_items() | ||||||
| 
 | 
 | ||||||
| @ -165,9 +167,31 @@ class ProductionPlan(Document): | |||||||
| 		self.calculate_total_planned_qty() | 		self.calculate_total_planned_qty() | ||||||
| 
 | 
 | ||||||
| 	def add_items(self, items): | 	def add_items(self, items): | ||||||
| 		self.set('po_items', []) | 		refs = {} | ||||||
| 		for data in items: | 		for data in items: | ||||||
| 			item_details = get_item_details(data.item_code) | 			item_details = get_item_details(data.item_code) | ||||||
|  | 			if self.combine_items:	 | ||||||
|  | 				if item_details.bom_no in refs: | ||||||
|  | 					refs[item_details.bom_no]['so_details'].append({ | ||||||
|  | 						'sales_order': data.parent, | ||||||
|  | 						'sales_order_item': data.name,  | ||||||
|  | 						'qty': data.pending_qty | ||||||
|  | 					}) | ||||||
|  | 					refs[item_details.bom_no]['qty'] += data.pending_qty | ||||||
|  | 					continue | ||||||
|  | 
 | ||||||
|  | 				else: | ||||||
|  | 					refs[item_details.bom_no] = { | ||||||
|  | 						'qty': data.pending_qty, | ||||||
|  | 						'po_item_ref': data.name, | ||||||
|  | 						'so_details': [] | ||||||
|  | 					} | ||||||
|  | 					refs[item_details.bom_no]['so_details'].append({ | ||||||
|  | 						'sales_order': data.parent, | ||||||
|  | 						'sales_order_item': data.name,  | ||||||
|  | 						'qty': data.pending_qty | ||||||
|  | 					}) | ||||||
|  | 					 | ||||||
| 			pi = self.append('po_items', { | 			pi = self.append('po_items', { | ||||||
| 				'include_exploded_items': 1, | 				'include_exploded_items': 1, | ||||||
| 				'warehouse': data.warehouse, | 				'warehouse': data.warehouse, | ||||||
| @ -185,11 +209,28 @@ class ProductionPlan(Document): | |||||||
| 				pi.sales_order = data.parent | 				pi.sales_order = data.parent | ||||||
| 				pi.sales_order_item = data.name | 				pi.sales_order_item = data.name | ||||||
| 				pi.description = data.description | 				pi.description = data.description | ||||||
| 
 | 					 | ||||||
| 			elif self.get_items_from == "Material Request": | 			elif self.get_items_from == "Material Request": | ||||||
| 				pi.material_request = data.parent | 				pi.material_request = data.parent | ||||||
| 				pi.material_request_item = data.name | 				pi.material_request_item = data.name | ||||||
| 				pi.description = data.description | 				pi.description = data.description | ||||||
|  | 	 | ||||||
|  | 		if refs: | ||||||
|  | 			for po_item in self.po_items: | ||||||
|  | 				po_item.planned_qty = refs[po_item.bom_no]['qty'] | ||||||
|  | 				po_item.pending_qty = refs[po_item.bom_no]['qty'] | ||||||
|  | 				po_item.sales_order = '' | ||||||
|  | 			self.add_pp_ref(refs) | ||||||
|  | 
 | ||||||
|  | 	def add_pp_ref(self, refs): | ||||||
|  | 		for bom_no in refs: | ||||||
|  | 			for so_detail in refs[bom_no]['so_details']: | ||||||
|  | 				self.append('prod_plan_references', { | ||||||
|  | 						'item_reference': refs[bom_no]['po_item_ref'], | ||||||
|  | 						'sales_order': so_detail['sales_order'], | ||||||
|  | 						'sales_order_item': so_detail['sales_order_item'], | ||||||
|  | 						'qty': so_detail['qty'] | ||||||
|  | 				}) | ||||||
| 
 | 
 | ||||||
| 	def calculate_total_planned_qty(self): | 	def calculate_total_planned_qty(self): | ||||||
| 		self.total_planned_qty = 0 | 		self.total_planned_qty = 0 | ||||||
|  | |||||||
| @ -100,7 +100,7 @@ class TestProductionPlan(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 	def test_production_plan_sales_orders(self): | 	def test_production_plan_sales_orders(self): | ||||||
| 		item = 'Test Production Item 1' | 		item = 'Test Production Item 1' | ||||||
| 		so = make_sales_order(item_code=item, qty=5) | 		so = make_sales_order(item_code=item, qty=1) | ||||||
| 		sales_order = so.name | 		sales_order = so.name | ||||||
| 		sales_order_item = so.items[0].name | 		sales_order_item = so.items[0].name | ||||||
| 
 | 
 | ||||||
| @ -124,8 +124,8 @@ class TestProductionPlan(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 		wo_doc = frappe.get_doc('Work Order', work_order) | 		wo_doc = frappe.get_doc('Work Order', work_order) | ||||||
| 		wo_doc.update({ | 		wo_doc.update({ | ||||||
| 			'wip_warehouse': '_Test Warehouse 1 - _TC', | 			'wip_warehouse': 'Work In Progress - _TC', | ||||||
| 			'fg_warehouse': '_Test Warehouse - _TC' | 			'fg_warehouse': 'Finished Goods - _TC' | ||||||
| 		}) | 		}) | ||||||
| 		wo_doc.submit() | 		wo_doc.submit() | ||||||
| 
 | 
 | ||||||
| @ -145,6 +145,58 @@ class TestProductionPlan(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 		self.assertEqual(sales_orders, []) | 		self.assertEqual(sales_orders, []) | ||||||
| 
 | 
 | ||||||
|  | 	def test_production_plan_combine_items(self): | ||||||
|  | 		item = 'Test Production Item 1' | ||||||
|  | 		so = make_sales_order(item_code=item, qty=1) | ||||||
|  | 
 | ||||||
|  | 		pln = frappe.new_doc('Production Plan') | ||||||
|  | 		pln.company = so.company | ||||||
|  | 		pln.get_items_from = 'Sales Order' | ||||||
|  | 		pln.append('sales_orders', { | ||||||
|  | 			'sales_order': so.name, | ||||||
|  | 			'sales_order_date': so.transaction_date, | ||||||
|  | 			'customer': so.customer, | ||||||
|  | 			'grand_total': so.grand_total | ||||||
|  | 		}) | ||||||
|  | 		so = make_sales_order(item_code=item, qty=2) | ||||||
|  | 		pln.append('sales_orders', { | ||||||
|  | 			'sales_order': so.name, | ||||||
|  | 			'sales_order_date': so.transaction_date, | ||||||
|  | 			'customer': so.customer, | ||||||
|  | 			'grand_total': so.grand_total | ||||||
|  | 		}) | ||||||
|  | 		pln.combine_items = 1 | ||||||
|  | 		pln.get_items() | ||||||
|  | 		pln.submit() | ||||||
|  | 
 | ||||||
|  | 		self.assertTrue(pln.po_items[0].planned_qty, 3)	 | ||||||
|  | 
 | ||||||
|  | 		pln.make_work_order() | ||||||
|  | 		work_order = frappe.db.get_value('Work Order', { | ||||||
|  | 			'production_plan_item': pln.po_items[0].name, | ||||||
|  | 			'production_plan': pln.name | ||||||
|  | 		}, 'name') | ||||||
|  | 
 | ||||||
|  | 		wo_doc = frappe.get_doc('Work Order', work_order) | ||||||
|  | 		wo_doc.update({ | ||||||
|  | 			'wip_warehouse': 'Work In Progress - _TC', | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		wo_doc.submit() | ||||||
|  | 		so_items = [] | ||||||
|  | 		for plan_reference in pln.prod_plan_references: | ||||||
|  | 			so_items.append(plan_reference.sales_order_item) | ||||||
|  | 			so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty') | ||||||
|  | 			self.assertEqual(so_wo_qty, plan_reference.qty) | ||||||
|  | 
 | ||||||
|  | 		wo_doc.cancel() | ||||||
|  | 		for so_item in so_items: | ||||||
|  | 			so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') | ||||||
|  | 			self.assertEqual(so_wo_qty, 0.0) | ||||||
|  | 		 | ||||||
|  | 		latest_plan = frappe.get_doc('Production Plan', pln.name) | ||||||
|  | 		latest_plan.cancel() | ||||||
|  | 	 | ||||||
| 	def test_pp_to_mr_customer_provided(self): | 	def test_pp_to_mr_customer_provided(self): | ||||||
| 		#Material Request from Production Plan for Customer Provided | 		#Material Request from Production Plan for Customer Provided | ||||||
| 		create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) | 		create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) | ||||||
|  | |||||||
| @ -1,792 +1,229 @@ | |||||||
| { | { | ||||||
|  "allow_copy": 0,  |  "actions": [], | ||||||
|  "allow_events_in_timeline": 0,  |  "autoname": "hash", | ||||||
|  "allow_guest_to_view": 0,  |  "creation": "2013-02-22 01:27:49", | ||||||
|  "allow_import": 0,  |  "doctype": "DocType", | ||||||
|  "allow_rename": 0,  |  "editable_grid": 1, | ||||||
|  "autoname": "hash",  |  "engine": "InnoDB", | ||||||
|  "beta": 0,  |  "field_order": [ | ||||||
|  "creation": "2013-02-22 01:27:49",  |   "include_exploded_items", | ||||||
|  "custom": 0,  |   "item_code", | ||||||
|  "docstatus": 0,  |   "bom_no", | ||||||
|  "doctype": "DocType",  |   "planned_qty", | ||||||
|  "editable_grid": 1,  |   "column_break_6", | ||||||
|  |   "make_work_order_for_sub_assembly_items", | ||||||
|  |   "warehouse", | ||||||
|  |   "planned_start_date", | ||||||
|  |   "section_break_9", | ||||||
|  |   "pending_qty", | ||||||
|  |   "ordered_qty", | ||||||
|  |   "produced_qty", | ||||||
|  |   "column_break_17", | ||||||
|  |   "description", | ||||||
|  |   "stock_uom", | ||||||
|  |   "reference_section", | ||||||
|  |   "sales_order", | ||||||
|  |   "sales_order_item", | ||||||
|  |   "column_break_19", | ||||||
|  |   "material_request", | ||||||
|  |   "material_request_item", | ||||||
|  |   "product_bundle_item", | ||||||
|  |   "item_reference" | ||||||
|  |  ], | ||||||
|  "fields": [ |  "fields": [ | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "columns": 2, | ||||||
|    "allow_in_quick_entry": 0,  |    "default": "0", | ||||||
|    "allow_on_submit": 0,  |    "fieldname": "include_exploded_items", | ||||||
|    "bold": 0,  |    "fieldtype": "Check", | ||||||
|    "collapsible": 0,  |    "in_list_view": 1, | ||||||
|    "columns": 2,  |    "label": "Include Exploded Items" | ||||||
|    "fetch_if_empty": 0,  |   }, | ||||||
|    "fieldname": "include_exploded_items",  |  | ||||||
|    "fieldtype": "Check",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Include Exploded Items",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "columns": 2, | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "item_code", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Link", | ||||||
|    "bold": 0,  |    "in_list_view": 1, | ||||||
|    "collapsible": 0,  |    "label": "Item Code", | ||||||
|    "columns": 2,  |    "oldfieldname": "item_code", | ||||||
|    "fetch_if_empty": 0,  |    "oldfieldtype": "Link", | ||||||
|    "fieldname": "item_code",  |    "options": "Item", | ||||||
|    "fieldtype": "Link",  |    "print_width": "150px", | ||||||
|    "hidden": 0,  |    "reqd": 1, | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Item Code",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "item_code",  |  | ||||||
|    "oldfieldtype": "Link",  |  | ||||||
|    "options": "Item",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "150px",  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 1,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "150px" |    "width": "150px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "columns": 2, | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "bom_no", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Link", | ||||||
|    "bold": 0,  |    "in_list_view": 1, | ||||||
|    "collapsible": 0,  |    "label": "BOM No", | ||||||
|    "columns": 2,  |    "oldfieldname": "bom_no", | ||||||
|    "fetch_if_empty": 0,  |    "oldfieldtype": "Link", | ||||||
|    "fieldname": "bom_no",  |    "options": "BOM", | ||||||
|    "fieldtype": "Link",  |    "print_width": "100px", | ||||||
|    "hidden": 0,  |    "reqd": 1, | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "BOM No",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "bom_no",  |  | ||||||
|    "oldfieldtype": "Link",  |  | ||||||
|    "options": "BOM",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "100px",  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 1,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "100px" |    "width": "100px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "planned_qty", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Float", | ||||||
|    "allow_on_submit": 0,  |    "in_list_view": 1, | ||||||
|    "bold": 0,  |    "label": "Planned Qty", | ||||||
|    "collapsible": 0,  |    "oldfieldname": "planned_qty", | ||||||
|    "columns": 0,  |    "oldfieldtype": "Currency", | ||||||
|    "fetch_if_empty": 0,  |    "print_width": "100px", | ||||||
|    "fieldname": "planned_qty",  |    "reqd": 1, | ||||||
|    "fieldtype": "Float",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Planned Qty",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "planned_qty",  |  | ||||||
|    "oldfieldtype": "Currency",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "100px",  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 1,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "100px" |    "width": "100px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "column_break_6", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Column Break" | ||||||
|    "allow_on_submit": 0,  |   }, | ||||||
|    "bold": 0,  |  | ||||||
|    "collapsible": 0,  |  | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "column_break_6",  |  | ||||||
|    "fieldtype": "Column Break",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "default": "0", | ||||||
|    "allow_in_quick_entry": 0,  |    "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", | ||||||
|    "allow_on_submit": 0,  |    "fieldname": "make_work_order_for_sub_assembly_items", | ||||||
|    "bold": 0,  |    "fieldtype": "Check", | ||||||
|    "collapsible": 0,  |    "label": "Make Work Order for Sub Assembly Items" | ||||||
|    "columns": 0,  |   }, | ||||||
|    "depends_on": "",  |  | ||||||
|    "description": "If enabled, system will create the work order for the exploded items against which BOM is available.",  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "make_work_order_for_sub_assembly_items",  |  | ||||||
|    "fieldtype": "Check",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Make Work Order for Sub Assembly Items",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "warehouse", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Link", | ||||||
|    "allow_on_submit": 0,  |    "in_list_view": 1, | ||||||
|    "bold": 0,  |    "label": "For Warehouse", | ||||||
|    "collapsible": 0,  |    "options": "Warehouse" | ||||||
|    "columns": 0,  |   }, | ||||||
|    "description": "",  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "warehouse",  |  | ||||||
|    "fieldtype": "Link",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "For Warehouse",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "options": "Warehouse",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "default": "Today", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "planned_start_date", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Datetime", | ||||||
|    "bold": 0,  |    "in_list_view": 1, | ||||||
|    "collapsible": 0,  |    "label": "Planned Start Date", | ||||||
|    "columns": 0,  |    "reqd": 1 | ||||||
|    "default": "Today",  |   }, | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "planned_start_date",  |  | ||||||
|    "fieldtype": "Datetime",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 1,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Planned Start Date",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 1,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "section_break_9", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Section Break", | ||||||
|    "allow_on_submit": 0,  |    "label": "Quantity and Description" | ||||||
|    "bold": 0,  |   }, | ||||||
|    "collapsible": 0,  |  | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "section_break_9",  |  | ||||||
|    "fieldtype": "Section Break",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Quantity and Description",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "default": "0", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "pending_qty", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Float", | ||||||
|    "bold": 0,  |    "label": "Pending Qty", | ||||||
|    "collapsible": 0,  |    "oldfieldname": "prevdoc_reqd_qty", | ||||||
|    "columns": 0,  |    "oldfieldtype": "Currency", | ||||||
|    "default": "0",  |    "print_width": "100px", | ||||||
|    "fetch_if_empty": 0,  |    "read_only": 1, | ||||||
|    "fieldname": "pending_qty",  |  | ||||||
|    "fieldtype": "Float",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Pending Qty",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "prevdoc_reqd_qty",  |  | ||||||
|    "oldfieldtype": "Currency",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "100px",  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "100px" |    "width": "100px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "default": "0", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "ordered_qty", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Float", | ||||||
|    "bold": 0,  |    "label": "Ordered Qty", | ||||||
|    "collapsible": 0,  |    "print_hide": 1, | ||||||
|    "columns": 0,  |    "read_only": 1 | ||||||
|    "default": "0",  |   }, | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "ordered_qty",  |  | ||||||
|    "fieldtype": "Float",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Ordered Qty",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 1,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "default": "0", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldname": "produced_qty", | ||||||
|    "allow_on_submit": 0,  |    "fieldtype": "Float", | ||||||
|    "bold": 0,  |    "label": "Produced Qty", | ||||||
|    "collapsible": 0,  |    "no_copy": 1, | ||||||
|    "columns": 0,  |    "read_only": 1 | ||||||
|    "default": "0",  |   }, | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "produced_qty",  |  | ||||||
|    "fieldtype": "Float",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Produced Qty",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 1,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "column_break_17", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Column Break" | ||||||
|    "allow_on_submit": 0,  |   }, | ||||||
|    "bold": 0,  |  | ||||||
|    "collapsible": 0,  |  | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "column_break_17",  |  | ||||||
|    "fieldtype": "Column Break",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "description", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Text Editor", | ||||||
|    "allow_on_submit": 0,  |    "label": "Description", | ||||||
|    "bold": 0,  |    "oldfieldname": "description", | ||||||
|    "collapsible": 0,  |    "oldfieldtype": "Text", | ||||||
|    "columns": 0,  |    "print_width": "200px", | ||||||
|    "fetch_if_empty": 0,  |    "read_only": 1, | ||||||
|    "fieldname": "description",  |  | ||||||
|    "fieldtype": "Text Editor",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Description",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "description",  |  | ||||||
|    "oldfieldtype": "Text",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "200px",  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "200px" |    "width": "200px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "stock_uom", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Link", | ||||||
|    "allow_on_submit": 0,  |    "label": "UOM", | ||||||
|    "bold": 0,  |    "oldfieldname": "stock_uom", | ||||||
|    "collapsible": 0,  |    "oldfieldtype": "Data", | ||||||
|    "columns": 0,  |    "options": "UOM", | ||||||
|    "fetch_if_empty": 0,  |    "print_width": "80px", | ||||||
|    "fieldname": "stock_uom",  |    "read_only": 1, | ||||||
|    "fieldtype": "Link",  |    "reqd": 1, | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "UOM",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "stock_uom",  |  | ||||||
|    "oldfieldtype": "Data",  |  | ||||||
|    "options": "UOM",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "print_width": "80px",  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 1,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0,  |  | ||||||
|    "width": "80px" |    "width": "80px" | ||||||
|   },  |   }, | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "reference_section", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Section Break", | ||||||
|    "allow_on_submit": 0,  |    "label": "Reference" | ||||||
|    "bold": 0,  |   }, | ||||||
|    "collapsible": 0,  |  | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "reference_section",  |  | ||||||
|    "fieldtype": "Section Break",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Reference",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "sales_order", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Link", | ||||||
|    "allow_on_submit": 0,  |    "label": "Sales Order", | ||||||
|    "bold": 0,  |    "oldfieldname": "source_docname", | ||||||
|    "collapsible": 0,  |    "oldfieldtype": "Data", | ||||||
|    "columns": 0,  |    "options": "Sales Order", | ||||||
|    "fetch_if_empty": 0,  |    "read_only": 1 | ||||||
|    "fieldname": "sales_order",  |   }, | ||||||
|    "fieldtype": "Link",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Sales Order",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "oldfieldname": "source_docname",  |  | ||||||
|    "oldfieldtype": "Data",  |  | ||||||
|    "options": "Sales Order",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "sales_order_item", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Data", | ||||||
|    "allow_on_submit": 0,  |    "hidden": 1, | ||||||
|    "bold": 0,  |    "label": "Sales Order Item", | ||||||
|    "collapsible": 0,  |    "no_copy": 1, | ||||||
|    "columns": 0,  |    "print_hide": 1 | ||||||
|    "fetch_if_empty": 0,  |   }, | ||||||
|    "fieldname": "sales_order_item",  |  | ||||||
|    "fieldtype": "Data",  |  | ||||||
|    "hidden": 1,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Sales Order Item",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 1,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 1,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "column_break_19", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Column Break" | ||||||
|    "allow_on_submit": 0,  |   }, | ||||||
|    "bold": 0,  |  | ||||||
|    "collapsible": 0,  |  | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "column_break_19",  |  | ||||||
|    "fieldtype": "Column Break",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "material_request", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Link", | ||||||
|    "allow_on_submit": 0,  |    "label": "Material Request", | ||||||
|    "bold": 0,  |    "options": "Material Request", | ||||||
|    "collapsible": 0,  |    "read_only": 1 | ||||||
|    "columns": 0,  |   }, | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "material_request",  |  | ||||||
|    "fieldtype": "Link",  |  | ||||||
|    "hidden": 0,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Material Request",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "options": "Material Request",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "material_request_item", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Data", | ||||||
|    "allow_on_submit": 0,  |    "hidden": 1, | ||||||
|    "bold": 0,  |    "label": "material_request_item" | ||||||
|    "collapsible": 0,  |   }, | ||||||
|    "columns": 0,  |  | ||||||
|    "fetch_if_empty": 0,  |  | ||||||
|    "fieldname": "material_request_item",  |  | ||||||
|    "fieldtype": "Data",  |  | ||||||
|    "hidden": 1,  |  | ||||||
|    "ignore_user_permissions": 0,  |  | ||||||
|    "ignore_xss_filter": 0,  |  | ||||||
|    "in_filter": 0,  |  | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "material_request_item",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 0,  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 0,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 0,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   },  |  | ||||||
|   { |   { | ||||||
|    "allow_bulk_edit": 0,  |    "fieldname": "product_bundle_item", | ||||||
|    "allow_in_quick_entry": 0,  |    "fieldtype": "Link", | ||||||
|    "allow_on_submit": 0,  |    "label": "Product Bundle Item", | ||||||
|    "bold": 0,  |    "no_copy": 1, | ||||||
|    "collapsible": 0,  |    "options": "Item", | ||||||
|    "columns": 0,  |    "print_hide": 1, | ||||||
|    "fetch_if_empty": 0,  |    "read_only": 1 | ||||||
|    "fieldname": "product_bundle_item",  |   }, | ||||||
|    "fieldtype": "Link",  |   { | ||||||
|    "hidden": 0,  |    "fieldname": "item_reference", | ||||||
|    "ignore_user_permissions": 0,  |    "fieldtype": "Data", | ||||||
|    "ignore_xss_filter": 0,  |    "hidden": 1, | ||||||
|    "in_filter": 0,  |    "label": "Item Reference" | ||||||
|    "in_global_search": 0,  |  | ||||||
|    "in_list_view": 0,  |  | ||||||
|    "in_standard_filter": 0,  |  | ||||||
|    "label": "Product Bundle Item",  |  | ||||||
|    "length": 0,  |  | ||||||
|    "no_copy": 1,  |  | ||||||
|    "options": "Item",  |  | ||||||
|    "permlevel": 0,  |  | ||||||
|    "precision": "",  |  | ||||||
|    "print_hide": 1,  |  | ||||||
|    "print_hide_if_no_value": 0,  |  | ||||||
|    "read_only": 1,  |  | ||||||
|    "remember_last_selected_value": 0,  |  | ||||||
|    "report_hide": 0,  |  | ||||||
|    "reqd": 0,  |  | ||||||
|    "search_index": 0,  |  | ||||||
|    "set_only_once": 0,  |  | ||||||
|    "translatable": 0,  |  | ||||||
|    "unique": 0 |  | ||||||
|   } |   } | ||||||
|  ],  |  ], | ||||||
|  "has_web_view": 0,  |  "idx": 1, | ||||||
|  "hide_toolbar": 0,  |  "istable": 1, | ||||||
|  "idx": 1,  |  "links": [], | ||||||
|  "in_create": 0,  |  "modified": "2021-04-28 19:14:57.772123", | ||||||
|  "is_submittable": 0,  |  "modified_by": "Administrator", | ||||||
|  "issingle": 0,  |  "module": "Manufacturing", | ||||||
|  "istable": 1,  |  "name": "Production Plan Item", | ||||||
|  "max_attachments": 0,  |  "owner": "Administrator", | ||||||
|  "modified": "2019-04-08 23:09:57.199423",  |  "permissions": [], | ||||||
|  "modified_by": "Administrator",  |  "sort_field": "modified", | ||||||
|  "module": "Manufacturing",  |  "sort_order": "ASC" | ||||||
|  "name": "Production Plan Item",  |  | ||||||
|  "owner": "Administrator",  |  | ||||||
|  "permissions": [],  |  | ||||||
|  "quick_entry": 0,  |  | ||||||
|  "read_only": 0,  |  | ||||||
|  "show_name_in_global_search": 0,  |  | ||||||
|  "sort_order": "ASC",  |  | ||||||
|  "track_changes": 0,  |  | ||||||
|  "track_seen": 0,  |  | ||||||
|  "track_views": 0 |  | ||||||
| } | } | ||||||
| @ -0,0 +1,52 @@ | |||||||
|  | { | ||||||
|  |  "actions": [], | ||||||
|  |  "creation": "2021-04-22 10:32:58.896330", | ||||||
|  |  "doctype": "DocType", | ||||||
|  |  "editable_grid": 1, | ||||||
|  |  "engine": "InnoDB", | ||||||
|  |  "field_order": [ | ||||||
|  |   "item_reference", | ||||||
|  |   "sales_order", | ||||||
|  |   "sales_order_item", | ||||||
|  |   "qty" | ||||||
|  |  ], | ||||||
|  |  "fields": [ | ||||||
|  |   { | ||||||
|  |    "fieldname": "sales_order", | ||||||
|  |    "fieldtype": "Link", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "Sales Order Reference", | ||||||
|  |    "options": "Sales Order" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "sales_order_item", | ||||||
|  |    "fieldtype": "Data", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "Sales Order Item" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "qty", | ||||||
|  |    "fieldtype": "Data", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "qty" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "item_reference", | ||||||
|  |    "fieldtype": "Data", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "Item Reference" | ||||||
|  |   } | ||||||
|  |  ], | ||||||
|  |  "index_web_pages_for_search": 1, | ||||||
|  |  "istable": 1, | ||||||
|  |  "links": [], | ||||||
|  |  "modified": "2021-05-07 17:03:49.707487", | ||||||
|  |  "modified_by": "Administrator", | ||||||
|  |  "module": "Manufacturing", | ||||||
|  |  "name": "Production Plan Item Reference", | ||||||
|  |  "owner": "Administrator", | ||||||
|  |  "permissions": [], | ||||||
|  |  "sort_field": "modified", | ||||||
|  |  "sort_order": "DESC", | ||||||
|  |  "track_changes": 1 | ||||||
|  | } | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors | ||||||
|  | # For license information, please see license.txt | ||||||
|  | 
 | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # import frappe | ||||||
|  | from frappe.model.document import Document | ||||||
|  | 
 | ||||||
|  | class ProductionPlanItemReference(Document): | ||||||
|  | 	pass | ||||||
| @ -240,8 +240,12 @@ class WorkOrder(Document): | |||||||
| 			frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) | 			frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) | ||||||
| 		if not self.fg_warehouse: | 		if not self.fg_warehouse: | ||||||
| 			frappe.throw(_("For Warehouse is required before Submit")) | 			frappe.throw(_("For Warehouse is required before Submit")) | ||||||
|  | 		 | ||||||
|  | 		if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): | ||||||
|  | 			self.update_work_order_qty_in_combined_so() | ||||||
|  | 		else: | ||||||
|  | 			self.update_work_order_qty_in_so() | ||||||
| 
 | 
 | ||||||
| 		self.update_work_order_qty_in_so() |  | ||||||
| 		self.update_reserved_qty_for_production() | 		self.update_reserved_qty_for_production() | ||||||
| 		self.update_completed_qty_in_material_request() | 		self.update_completed_qty_in_material_request() | ||||||
| 		self.update_planned_qty() | 		self.update_planned_qty() | ||||||
| @ -250,9 +254,13 @@ class WorkOrder(Document): | |||||||
| 
 | 
 | ||||||
| 	def on_cancel(self): | 	def on_cancel(self): | ||||||
| 		self.validate_cancel() | 		self.validate_cancel() | ||||||
| 
 |  | ||||||
| 		frappe.db.set(self,'status', 'Cancelled') | 		frappe.db.set(self,'status', 'Cancelled') | ||||||
| 		self.update_work_order_qty_in_so() | 
 | ||||||
|  | 		if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): | ||||||
|  | 			self.update_work_order_qty_in_combined_so() | ||||||
|  | 		else: | ||||||
|  | 			self.update_work_order_qty_in_so() | ||||||
|  | 			 | ||||||
| 		self.delete_job_card() | 		self.delete_job_card() | ||||||
| 		self.update_completed_qty_in_material_request() | 		self.update_completed_qty_in_material_request() | ||||||
| 		self.update_planned_qty() | 		self.update_planned_qty() | ||||||
| @ -357,7 +365,28 @@ class WorkOrder(Document): | |||||||
| 		work_order_qty = qty[0][0] if qty and qty[0][0] else 0 | 		work_order_qty = qty[0][0] if qty and qty[0][0] else 0 | ||||||
| 		frappe.db.set_value('Sales Order Item', | 		frappe.db.set_value('Sales Order Item', | ||||||
| 			self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) | 			self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) | ||||||
|  | 		 | ||||||
|  | 	def update_work_order_qty_in_combined_so(self): | ||||||
|  | 		total_bundle_qty = 1 | ||||||
|  | 		if self.product_bundle_item: | ||||||
|  | 			total_bundle_qty = frappe.db.sql(""" select sum(qty) from | ||||||
|  | 				`tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0] | ||||||
| 
 | 
 | ||||||
|  | 			if not total_bundle_qty: | ||||||
|  | 				# product bundle is 0 (product bundle allows 0 qty for items) | ||||||
|  | 				total_bundle_qty = 1 | ||||||
|  | 
 | ||||||
|  | 		prod_plan = frappe.get_doc('Production Plan', self.production_plan) | ||||||
|  | 		item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') | ||||||
|  | 		 | ||||||
|  | 		for plan_reference in prod_plan.prod_plan_references: | ||||||
|  | 			work_order_qty = 0.0 | ||||||
|  | 			if plan_reference.item_reference == item_reference: | ||||||
|  | 				if self.docstatus == 1: | ||||||
|  | 					work_order_qty = flt(plan_reference.qty) / total_bundle_qty | ||||||
|  | 				frappe.db.set_value('Sales Order Item', | ||||||
|  | 					plan_reference.sales_order_item, 'work_order_qty', work_order_qty) | ||||||
|  | 	 | ||||||
| 	def update_completed_qty_in_material_request(self): | 	def update_completed_qty_in_material_request(self): | ||||||
| 		if self.material_request: | 		if self.material_request: | ||||||
| 			frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) | 			frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) | ||||||
|  | |||||||
| @ -261,11 +261,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ | |||||||
| 		if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) { | 		if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) { | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| 		var me = this; | 
 | ||||||
| 		var inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) | 		const me = this; | ||||||
|  | 		if (!this.frm.is_new() && this.frm.doc.docstatus === 0) { | ||||||
|  | 			this.frm.add_custom_button(__("Quality Inspection(s)"), () => { | ||||||
|  | 				me.make_quality_inspection(); | ||||||
|  | 			}, __("Create")); | ||||||
|  | 			this.frm.page.set_inner_btn_group_as_primary(__('Create')); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) | ||||||
| 			? "Incoming" : "Outgoing"; | 			? "Incoming" : "Outgoing"; | ||||||
| 
 | 
 | ||||||
| 		var quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); | 		let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); | ||||||
| 		quality_inspection_field.get_route_options_for_new_doc = function(row) { | 		quality_inspection_field.get_route_options_for_new_doc = function(row) { | ||||||
| 			if(me.frm.is_new()) return; | 			if(me.frm.is_new()) return; | ||||||
| 			return { | 			return { | ||||||
| @ -280,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { | 		this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { | ||||||
| 			var d = locals[cdt][cdn]; | 			let d = locals[cdt][cdn]; | ||||||
| 			return { | 			return { | ||||||
| 				filters: { | 				filters: { | ||||||
| 					docstatus: 1, | 					docstatus: 1, | ||||||
| @ -1949,6 +1957,130 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ | |||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	make_quality_inspection: function () { | ||||||
|  | 		let data = []; | ||||||
|  | 		const fields = [ | ||||||
|  | 			{ | ||||||
|  | 				label: "Items", | ||||||
|  | 				fieldtype: "Table", | ||||||
|  | 				fieldname: "items", | ||||||
|  | 				cannot_add_rows: true, | ||||||
|  | 				in_place_edit: true, | ||||||
|  | 				data: data, | ||||||
|  | 				get_data: () => { | ||||||
|  | 					return data; | ||||||
|  | 				}, | ||||||
|  | 				fields: [ | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Data", | ||||||
|  | 						fieldname: "docname", | ||||||
|  | 						hidden: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Read Only", | ||||||
|  | 						fieldname: "item_code", | ||||||
|  | 						label: __("Item Code"), | ||||||
|  | 						in_list_view: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Read Only", | ||||||
|  | 						fieldname: "item_name", | ||||||
|  | 						label: __("Item Name"), | ||||||
|  | 						in_list_view: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Float", | ||||||
|  | 						fieldname: "qty", | ||||||
|  | 						label: __("Accepted Quantity"), | ||||||
|  | 						in_list_view: true, | ||||||
|  | 						read_only: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Float", | ||||||
|  | 						fieldname: "sample_size", | ||||||
|  | 						label: __("Sample Size"), | ||||||
|  | 						reqd: true, | ||||||
|  | 						in_list_view: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Data", | ||||||
|  | 						fieldname: "description", | ||||||
|  | 						label: __("Description"), | ||||||
|  | 						hidden: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Data", | ||||||
|  | 						fieldname: "serial_no", | ||||||
|  | 						label: __("Serial No"), | ||||||
|  | 						hidden: true | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						fieldtype: "Data", | ||||||
|  | 						fieldname: "batch_no", | ||||||
|  | 						label: __("Batch No"), | ||||||
|  | 						hidden: true | ||||||
|  | 					} | ||||||
|  | 				] | ||||||
|  | 			} | ||||||
|  | 		]; | ||||||
|  | 
 | ||||||
|  | 		const me = this; | ||||||
|  | 		const dialog = new frappe.ui.Dialog({ | ||||||
|  | 			title: __("Select Items for Quality Inspection"), | ||||||
|  | 			fields: fields, | ||||||
|  | 			primary_action: function () { | ||||||
|  | 				const data = dialog.get_values(); | ||||||
|  | 				frappe.call({ | ||||||
|  | 					method: "erpnext.controllers.stock_controller.make_quality_inspections", | ||||||
|  | 					args: { | ||||||
|  | 						doctype: me.frm.doc.doctype, | ||||||
|  | 						docname: me.frm.doc.name, | ||||||
|  | 						items: data.items | ||||||
|  | 					}, | ||||||
|  | 					freeze: true, | ||||||
|  | 					callback: function (r) { | ||||||
|  | 						if (r.message.length > 0) { | ||||||
|  | 							if (r.message.length === 1) { | ||||||
|  | 								frappe.set_route("Form", "Quality Inspection", r.message[0]); | ||||||
|  | 							} else { | ||||||
|  | 								frappe.route_options = { | ||||||
|  | 									"reference_type": me.frm.doc.doctype, | ||||||
|  | 									"reference_name": me.frm.doc.name | ||||||
|  | 								}; | ||||||
|  | 								frappe.set_route("List", "Quality Inspection"); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 						dialog.hide(); | ||||||
|  | 					} | ||||||
|  | 				}); | ||||||
|  | 			}, | ||||||
|  | 			primary_action_label: __("Create") | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		this.frm.doc.items.forEach(item => { | ||||||
|  | 			if (!item.quality_inspection) { | ||||||
|  | 				let dialog_items = dialog.fields_dict.items; | ||||||
|  | 				dialog_items.df.data.push({ | ||||||
|  | 					"docname": item.name, | ||||||
|  | 					"item_code": item.item_code, | ||||||
|  | 					"item_name": item.item_name, | ||||||
|  | 					"qty": item.qty, | ||||||
|  | 					"description": item.description, | ||||||
|  | 					"serial_no": item.serial_no, | ||||||
|  | 					"batch_no": item.batch_no | ||||||
|  | 				}); | ||||||
|  | 				dialog_items.grid.refresh(); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		data = dialog.fields_dict.items.df.data; | ||||||
|  | 		if (!data.length) { | ||||||
|  | 			frappe.msgprint(__("All items in this document already have a linked Quality Inspection.")); | ||||||
|  | 		} else { | ||||||
|  | 			dialog.show(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	get_method_for_payment: function(){ | 	get_method_for_payment: function(){ | ||||||
| 		var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; | 		var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; | ||||||
| 		if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ | 		if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | ||||||
| # License: GNU General Public License v3. See license.txt | # License: GNU General Public License v3. See license.txt | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals |  | ||||||
| 
 |  | ||||||
| import itertools | import itertools | ||||||
| import json | import json | ||||||
| import erpnext | import erpnext | ||||||
| @ -12,7 +10,7 @@ from erpnext.controllers.item_variant import (ItemVariantExistsError, | |||||||
| 		copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes) | 		copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes) | ||||||
| from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) | from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) | ||||||
| from frappe import _, msgprint | from frappe import _, msgprint | ||||||
| from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, | from frappe.utils import (cint, cstr, flt, formatdate, getdate, | ||||||
| 		now_datetime, random_string, strip, get_link_to_form, nowtime) | 		now_datetime, random_string, strip, get_link_to_form, nowtime) | ||||||
| from frappe.utils.html_utils import clean_html | from frappe.utils.html_utils import clean_html | ||||||
| from frappe.website.doctype.website_slideshow.website_slideshow import \ | from frappe.website.doctype.website_slideshow.website_slideshow import \ | ||||||
| @ -21,8 +19,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import \ | |||||||
| from frappe.website.render import clear_cache | from frappe.website.render import clear_cache | ||||||
| from frappe.website.website_generator import WebsiteGenerator | from frappe.website.website_generator import WebsiteGenerator | ||||||
| 
 | 
 | ||||||
| from six import iteritems |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class DuplicateReorderRows(frappe.ValidationError): | class DuplicateReorderRows(frappe.ValidationError): | ||||||
| 	pass | 	pass | ||||||
| @ -76,8 +72,6 @@ class Item(WebsiteGenerator): | |||||||
| 		if not self.description: | 		if not self.description: | ||||||
| 			self.description = self.item_name | 			self.description = self.item_name | ||||||
| 
 | 
 | ||||||
| 		# if self.is_sales_item and not self.get('is_item_from_hub'): |  | ||||||
| 		# 	self.publish_in_hub = 1 |  | ||||||
| 
 | 
 | ||||||
| 	def after_insert(self): | 	def after_insert(self): | ||||||
| 		'''set opening stock and item price''' | 		'''set opening stock and item price''' | ||||||
| @ -129,7 +123,7 @@ class Item(WebsiteGenerator): | |||||||
| 		self.cant_change() | 		self.cant_change() | ||||||
| 		self.update_show_in_website() | 		self.update_show_in_website() | ||||||
| 
 | 
 | ||||||
| 		if not self.get("__islocal"): | 		if not self.is_new(): | ||||||
| 			self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") | 			self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") | ||||||
| 			self.old_website_item_groups = frappe.db.sql_list("""select item_group | 			self.old_website_item_groups = frappe.db.sql_list("""select item_group | ||||||
| 					from `tabWebsite Item Group` | 					from `tabWebsite Item Group` | ||||||
| @ -203,7 +197,7 @@ class Item(WebsiteGenerator): | |||||||
| 	def make_route(self): | 	def make_route(self): | ||||||
| 		if not self.route: | 		if not self.route: | ||||||
| 			return cstr(frappe.db.get_value('Item Group', self.item_group, | 			return cstr(frappe.db.get_value('Item Group', self.item_group, | ||||||
| 					'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) | 					'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) | ||||||
| 
 | 
 | ||||||
| 	def validate_website_image(self): | 	def validate_website_image(self): | ||||||
| 		if frappe.flags.in_import: | 		if frappe.flags.in_import: | ||||||
| @ -258,7 +252,6 @@ class Item(WebsiteGenerator): | |||||||
| 					"attached_to_name": self.name | 					"attached_to_name": self.name | ||||||
| 				}) | 				}) | ||||||
| 			except frappe.DoesNotExistError: | 			except frappe.DoesNotExistError: | ||||||
| 				pass |  | ||||||
| 				# cleanup | 				# cleanup | ||||||
| 				frappe.local.message_log.pop() | 				frappe.local.message_log.pop() | ||||||
| 
 | 
 | ||||||
| @ -362,47 +355,49 @@ class Item(WebsiteGenerator): | |||||||
| 				context.update(get_slideshow(self)) | 				context.update(get_slideshow(self)) | ||||||
| 
 | 
 | ||||||
| 	def set_attribute_context(self, context): | 	def set_attribute_context(self, context): | ||||||
| 		if self.has_variants: | 		if not self.has_variants: | ||||||
| 			attribute_values_available = {} | 			return | ||||||
| 			context.attribute_values = {} |  | ||||||
| 			context.selected_attributes = {} |  | ||||||
| 
 | 
 | ||||||
| 			# load attributes | 		attribute_values_available = {} | ||||||
| 			for v in context.variants: | 		context.attribute_values = {} | ||||||
| 				v.attributes = frappe.get_all("Item Variant Attribute", | 		context.selected_attributes = {} | ||||||
| 					  fields=["attribute", "attribute_value"], |  | ||||||
| 					  filters={"parent": v.name}) |  | ||||||
| 				# make a map for easier access in templates |  | ||||||
| 				v.attribute_map = frappe._dict({}) |  | ||||||
| 				for attr in v.attributes: |  | ||||||
| 					v.attribute_map[attr.attribute] = attr.attribute_value |  | ||||||
| 
 | 
 | ||||||
| 				for attr in v.attributes: | 		# load attributes | ||||||
| 					values = attribute_values_available.setdefault(attr.attribute, []) | 		for v in context.variants: | ||||||
| 					if attr.attribute_value not in values: | 			v.attributes = frappe.get_all("Item Variant Attribute", | ||||||
| 						values.append(attr.attribute_value) | 				fields=["attribute", "attribute_value"], | ||||||
|  | 				filters={"parent": v.name}) | ||||||
|  | 			# make a map for easier access in templates | ||||||
|  | 			v.attribute_map = frappe._dict({}) | ||||||
|  | 			for attr in v.attributes: | ||||||
|  | 				v.attribute_map[attr.attribute] = attr.attribute_value | ||||||
| 
 | 
 | ||||||
| 					if v.name == context.variant.name: | 			for attr in v.attributes: | ||||||
| 						context.selected_attributes[attr.attribute] = attr.attribute_value | 				values = attribute_values_available.setdefault(attr.attribute, []) | ||||||
|  | 				if attr.attribute_value not in values: | ||||||
|  | 					values.append(attr.attribute_value) | ||||||
| 
 | 
 | ||||||
| 			# filter attributes, order based on attribute table | 				if v.name == context.variant.name: | ||||||
| 			for attr in self.attributes: | 					context.selected_attributes[attr.attribute] = attr.attribute_value | ||||||
| 				values = context.attribute_values.setdefault(attr.attribute, []) |  | ||||||
| 
 | 
 | ||||||
| 				if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): | 		# filter attributes, order based on attribute table | ||||||
| 					for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): | 		for attr in self.attributes: | ||||||
| 						values.append(val) | 			values = context.attribute_values.setdefault(attr.attribute, []) | ||||||
| 
 | 
 | ||||||
| 				else: | 			if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): | ||||||
| 					# get list of values defined (for sequence) | 				for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): | ||||||
| 					for attr_value in frappe.db.get_all("Item Attribute Value", | 					values.append(val) | ||||||
| 						fields=["attribute_value"], |  | ||||||
| 						filters={"parent": attr.attribute}, order_by="idx asc"): |  | ||||||
| 
 | 
 | ||||||
| 						if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): | 			else: | ||||||
| 							values.append(attr_value.attribute_value) | 				# get list of values defined (for sequence) | ||||||
|  | 				for attr_value in frappe.db.get_all("Item Attribute Value", | ||||||
|  | 					fields=["attribute_value"], | ||||||
|  | 					filters={"parent": attr.attribute}, order_by="idx asc"): | ||||||
| 
 | 
 | ||||||
| 			context.variant_info = json.dumps(context.variants) | 					if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): | ||||||
|  | 						values.append(attr_value.attribute_value) | ||||||
|  | 
 | ||||||
|  | 		context.variant_info = json.dumps(context.variants) | ||||||
| 
 | 
 | ||||||
| 	def set_disabled_attributes(self, context): | 	def set_disabled_attributes(self, context): | ||||||
| 		"""Disable selection options of attribute combinations that do not result in a variant""" | 		"""Disable selection options of attribute combinations that do not result in a variant""" | ||||||
| @ -521,7 +516,7 @@ class Item(WebsiteGenerator): | |||||||
| 
 | 
 | ||||||
| 	def validate_item_type(self): | 	def validate_item_type(self): | ||||||
| 		if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: | 		if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: | ||||||
| 			msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) | 			frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item")) | ||||||
| 
 | 
 | ||||||
| 		if self.has_serial_no == 0 and self.serial_no_series: | 		if self.has_serial_no == 0 and self.serial_no_series: | ||||||
| 			self.serial_no_series = None | 			self.serial_no_series = None | ||||||
| @ -542,10 +537,7 @@ class Item(WebsiteGenerator): | |||||||
| 
 | 
 | ||||||
| 	def fill_customer_code(self): | 	def fill_customer_code(self): | ||||||
| 		""" Append all the customer codes and insert into "customer_code" field of item table """ | 		""" Append all the customer codes and insert into "customer_code" field of item table """ | ||||||
| 		cust_code = [] | 		self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", [])) | ||||||
| 		for d in self.get('customer_items'): |  | ||||||
| 			cust_code.append(d.ref_code) |  | ||||||
| 		self.customer_code = ','.join(cust_code) |  | ||||||
| 
 | 
 | ||||||
| 	def check_item_tax(self): | 	def check_item_tax(self): | ||||||
| 		"""Check whether Tax Rate is not entered twice for same Tax Type""" | 		"""Check whether Tax Rate is not entered twice for same Tax Type""" | ||||||
| @ -742,23 +734,25 @@ class Item(WebsiteGenerator): | |||||||
| 
 | 
 | ||||||
| 	def update_template_item(self): | 	def update_template_item(self): | ||||||
| 		"""Set Show in Website for Template Item if True for its Variant""" | 		"""Set Show in Website for Template Item if True for its Variant""" | ||||||
| 		if self.variant_of: | 		if not self.variant_of: | ||||||
| 			if self.show_in_website: | 			return | ||||||
| 				self.show_variant_in_website = 1 |  | ||||||
| 				self.show_in_website = 0 |  | ||||||
| 
 | 
 | ||||||
| 			if self.show_variant_in_website: | 		if self.show_in_website: | ||||||
| 				# show template | 			self.show_variant_in_website = 1 | ||||||
| 				template_item = frappe.get_doc("Item", self.variant_of) | 			self.show_in_website = 0 | ||||||
| 
 | 
 | ||||||
| 				if not template_item.show_in_website: | 		if self.show_variant_in_website: | ||||||
| 					template_item.show_in_website = 1 | 			# show template | ||||||
| 					template_item.flags.dont_update_variants = True | 			template_item = frappe.get_doc("Item", self.variant_of) | ||||||
| 					template_item.flags.ignore_permissions = True | 
 | ||||||
| 					template_item.save() | 			if not template_item.show_in_website: | ||||||
|  | 				template_item.show_in_website = 1 | ||||||
|  | 				template_item.flags.dont_update_variants = True | ||||||
|  | 				template_item.flags.ignore_permissions = True | ||||||
|  | 				template_item.save() | ||||||
| 
 | 
 | ||||||
| 	def validate_item_defaults(self): | 	def validate_item_defaults(self): | ||||||
| 		companies = list(set([row.company for row in self.item_defaults])) | 		companies = {row.company for row in self.item_defaults} | ||||||
| 
 | 
 | ||||||
| 		if len(companies) != len(self.item_defaults): | 		if len(companies) != len(self.item_defaults): | ||||||
| 			frappe.throw(_("Cannot set multiple Item Defaults for a company.")) | 			frappe.throw(_("Cannot set multiple Item Defaults for a company.")) | ||||||
| @ -813,7 +807,7 @@ class Item(WebsiteGenerator): | |||||||
| 				frappe.throw(_("Item has variants.")) | 				frappe.throw(_("Item has variants.")) | ||||||
| 
 | 
 | ||||||
| 	def validate_attributes_in_variants(self): | 	def validate_attributes_in_variants(self): | ||||||
| 		if not self.has_variants or self.get("__islocal"): | 		if not self.has_variants or self.is_new(): | ||||||
| 			return | 			return | ||||||
| 
 | 
 | ||||||
| 		old_doc = self.get_doc_before_save() | 		old_doc = self.get_doc_before_save() | ||||||
| @ -901,7 +895,7 @@ class Item(WebsiteGenerator): | |||||||
| 				frappe.throw(_("Variant Based On cannot be changed")) | 				frappe.throw(_("Variant Based On cannot be changed")) | ||||||
| 
 | 
 | ||||||
| 	def validate_uom(self): | 	def validate_uom(self): | ||||||
| 		if not self.get("__islocal"): | 		if not self.is_new(): | ||||||
| 			check_stock_uom_with_bin(self.name, self.stock_uom) | 			check_stock_uom_with_bin(self.name, self.stock_uom) | ||||||
| 		if self.has_variants: | 		if self.has_variants: | ||||||
| 			for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): | 			for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): | ||||||
| @ -959,20 +953,20 @@ class Item(WebsiteGenerator): | |||||||
| 				d.variant_of = self.variant_of | 				d.variant_of = self.variant_of | ||||||
| 
 | 
 | ||||||
| 	def cant_change(self): | 	def cant_change(self): | ||||||
| 		if not self.get("__islocal"): | 		if self.is_new(): | ||||||
| 			fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") | 			return | ||||||
| 
 | 
 | ||||||
| 			values = frappe.db.get_value("Item", self.name, fields, as_dict=True) | 		fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") | ||||||
| 			if not values.get('valuation_method') and self.get('valuation_method'): |  | ||||||
| 				values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" |  | ||||||
| 
 | 
 | ||||||
| 			if values: | 		values = frappe.db.get_value("Item", self.name, fields, as_dict=True) | ||||||
| 				for field in fields: | 		if not values.get('valuation_method') and self.get('valuation_method'): | ||||||
| 					if cstr(self.get(field)) != cstr(values.get(field)): | 			values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" | ||||||
| 						if not self.check_if_linked_document_exists(field): | 
 | ||||||
| 							break # no linked document, allowed | 		if values: | ||||||
| 						else: | 			for field in fields: | ||||||
| 							frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) | 				if cstr(self.get(field)) != cstr(values.get(field)): | ||||||
|  | 					if self.check_if_linked_document_exists(field): | ||||||
|  | 						frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) | ||||||
| 
 | 
 | ||||||
| 	def check_if_linked_document_exists(self, field): | 	def check_if_linked_document_exists(self, field): | ||||||
| 		linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item", | 		linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item", | ||||||
| @ -1054,56 +1048,42 @@ def make_item_price(item, price_list_name, item_price): | |||||||
| 	}).insert() | 	}).insert() | ||||||
| 
 | 
 | ||||||
| def get_timeline_data(doctype, name): | def get_timeline_data(doctype, name): | ||||||
| 	'''returns timeline data based on stock ledger entry''' | 	"""get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" | ||||||
| 	out = {} |  | ||||||
| 	items = dict(frappe.db.sql('''select posting_date, count(*) |  | ||||||
| 		from `tabStock Ledger Entry` where item_code=%s |  | ||||||
| 			and posting_date > date_sub(curdate(), interval 1 year) |  | ||||||
| 			group by posting_date''', name)) |  | ||||||
| 
 | 
 | ||||||
| 	for date, count in iteritems(items): | 	items = frappe.db.sql("""select unix_timestamp(posting_date), count(*) | ||||||
| 		timestamp = get_timestamp(date) | 							from `tabStock Ledger Entry` | ||||||
| 		out.update({timestamp: count}) | 							where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) | ||||||
|  | 							group by posting_date""", name) | ||||||
| 
 | 
 | ||||||
| 	return out | 	return dict(items) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1): | 
 | ||||||
|  | def validate_end_of_life(item_code, end_of_life=None, disabled=None): | ||||||
| 	if (not end_of_life) or (disabled is None): | 	if (not end_of_life) or (disabled is None): | ||||||
| 		end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) | 		end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) | ||||||
| 
 | 
 | ||||||
| 	if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): | 	if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): | ||||||
| 		msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) | 		frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))) | ||||||
| 		_msgprint(msg, verbose) |  | ||||||
| 
 | 
 | ||||||
| 	if disabled: | 	if disabled: | ||||||
| 		_msgprint(_("Item {0} is disabled").format(item_code), verbose) | 		frappe.throw(_("Item {0} is disabled").format(item_code)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): | def validate_is_stock_item(item_code, is_stock_item=None): | ||||||
| 	if not is_stock_item: | 	if not is_stock_item: | ||||||
| 		is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") | 		is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") | ||||||
| 
 | 
 | ||||||
| 	if is_stock_item != 1: | 	if is_stock_item != 1: | ||||||
| 		msg = _("Item {0} is not a stock Item").format(item_code) | 		frappe.throw(_("Item {0} is not a stock Item").format(item_code)) | ||||||
| 
 |  | ||||||
| 		_msgprint(msg, verbose) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def validate_cancelled_item(item_code, docstatus=None, verbose=1): | def validate_cancelled_item(item_code, docstatus=None): | ||||||
| 	if docstatus is None: | 	if docstatus is None: | ||||||
| 		docstatus = frappe.db.get_value("Item", item_code, "docstatus") | 		docstatus = frappe.db.get_value("Item", item_code, "docstatus") | ||||||
| 
 | 
 | ||||||
| 	if docstatus == 2: | 	if docstatus == 2: | ||||||
| 		msg = _("Item {0} is cancelled").format(item_code) | 		frappe.throw(_("Item {0} is cancelled").format(item_code)) | ||||||
| 		_msgprint(msg, verbose) |  | ||||||
| 
 |  | ||||||
| def _msgprint(msg, verbose): |  | ||||||
| 	if verbose: |  | ||||||
| 		msgprint(msg, raise_exception=True) |  | ||||||
| 	else: |  | ||||||
| 		raise frappe.ValidationError(msg) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): | def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): | ||||||
| 	"""returns last purchase details in stock uom""" | 	"""returns last purchase details in stock uom""" | ||||||
| @ -1203,27 +1183,25 @@ def check_stock_uom_with_bin(item, stock_uom): | |||||||
| 	if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): | 	if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	matched = True |  | ||||||
| 	ref_uom = frappe.db.get_value("Stock Ledger Entry", | 	ref_uom = frappe.db.get_value("Stock Ledger Entry", | ||||||
| 							   {"item_code": item}, "stock_uom") | 							   {"item_code": item}, "stock_uom") | ||||||
| 
 | 
 | ||||||
| 	if ref_uom: | 	if ref_uom: | ||||||
| 		if cstr(ref_uom) != cstr(stock_uom): | 		if cstr(ref_uom) != cstr(stock_uom): | ||||||
| 			matched = False | 			frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) | ||||||
| 	else: |  | ||||||
| 		bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) |  | ||||||
| 		for bin in bin_list: |  | ||||||
| 			if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 |  | ||||||
| 								or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): |  | ||||||
| 				matched = False |  | ||||||
| 				break |  | ||||||
| 
 | 
 | ||||||
| 		if matched and bin_list: | 	bin_list = frappe.db.sql(""" | ||||||
| 			frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) | 			select * from tabBin where item_code = %s | ||||||
|  | 				and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0) | ||||||
|  | 				and stock_uom != %s | ||||||
|  | 			""", (item, stock_uom), as_dict=1) | ||||||
|  | 
 | ||||||
|  | 	if bin_list: | ||||||
|  | 		frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item)) | ||||||
|  | 
 | ||||||
|  | 	# No SLE or documents against item. Bin UOM can be changed safely. | ||||||
|  | 	frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) | ||||||
| 
 | 
 | ||||||
| 	if not matched: |  | ||||||
| 		frappe.throw( |  | ||||||
| 			_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) |  | ||||||
| 
 | 
 | ||||||
| def get_item_defaults(item_code, company): | def get_item_defaults(item_code, company): | ||||||
| 	item = frappe.get_cached_doc('Item', item_code) | 	item = frappe.get_cached_doc('Item', item_code) | ||||||
| @ -1264,45 +1242,59 @@ def get_item_details(item_code, company=None): | |||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def get_uom_conv_factor(uom, stock_uom): | def get_uom_conv_factor(uom, stock_uom): | ||||||
| 	uoms = [uom, stock_uom] | 	""" Get UOM conversion factor from uom to stock_uom | ||||||
| 	value = "" | 		e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 | ||||||
| 	uom_details = frappe.db.sql("""select to_uom, from_uom, value from `tabUOM Conversion Factor`\ | 	""" | ||||||
| 		where to_uom in ({0}) | 	if uom == stock_uom: | ||||||
| 		""".format(', '.join([frappe.db.escape(i, percent=False) for i in uoms])), as_dict=True) | 		return 1.0 | ||||||
| 
 | 
 | ||||||
| 	for d in uom_details: | 	from_uom, to_uom = uom, stock_uom   # renaming for readability | ||||||
| 		if d.from_uom == stock_uom and d.to_uom == uom: |  | ||||||
| 			value = 1/flt(d.value) |  | ||||||
| 		elif d.from_uom == uom and d.to_uom == stock_uom: |  | ||||||
| 			value = d.value |  | ||||||
| 
 | 
 | ||||||
| 	if not value: | 	exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) | ||||||
| 		uom_stock = frappe.db.get_value("UOM Conversion Factor", {"to_uom": stock_uom}, ["from_uom", "value"], as_dict=1) | 	if exact_match: | ||||||
| 		uom_row = frappe.db.get_value("UOM Conversion Factor", {"to_uom": uom}, ["from_uom", "value"], as_dict=1) | 		return exact_match.value | ||||||
| 
 | 
 | ||||||
| 		if uom_stock and uom_row: | 	inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1) | ||||||
| 			if uom_stock.from_uom == uom_row.from_uom: | 	if inverse_match: | ||||||
| 				value = flt(uom_stock.value) * 1/flt(uom_row.value) | 		return 1 / inverse_match.value | ||||||
|  | 
 | ||||||
|  | 	# This attempts to try and get conversion from intermediate UOM. | ||||||
|  | 	# case: | ||||||
|  | 	#            g -> mg = 1000 | ||||||
|  | 	#            g -> kg = 0.001 | ||||||
|  | 	# therefore  kg -> mg = 1000  / 0.001 = 1,000,000 | ||||||
|  | 	intermediate_match = frappe.db.sql(""" | ||||||
|  | 			select (first.value / second.value) as value | ||||||
|  | 			from `tabUOM Conversion Factor` first | ||||||
|  | 			join `tabUOM Conversion Factor` second | ||||||
|  | 				on first.from_uom = second.from_uom | ||||||
|  | 			where | ||||||
|  | 				first.to_uom = %(to_uom)s | ||||||
|  | 				and second.to_uom = %(from_uom)s | ||||||
|  | 			limit 1 | ||||||
|  | 			""", {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1) | ||||||
|  | 
 | ||||||
|  | 	if intermediate_match: | ||||||
|  | 		return intermediate_match[0].value | ||||||
| 
 | 
 | ||||||
| 	return value |  | ||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def get_item_attribute(parent, attribute_value=''): | def get_item_attribute(parent, attribute_value=""): | ||||||
|  | 	"""Used for providing auto-completions in child table.""" | ||||||
| 	if not frappe.has_permission("Item"): | 	if not frappe.has_permission("Item"): | ||||||
| 		frappe.msgprint(_("No Permission"), raise_exception=1) | 		frappe.throw(_("No Permission")) | ||||||
| 
 | 
 | ||||||
| 	return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], | 	return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], | ||||||
| 		filters = {'parent': parent, 'attribute_value': ("like", "%%%s%%" % attribute_value)}) | 		filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")}) | ||||||
| 
 | 
 | ||||||
| def update_variants(variants, template, publish_progress=True): | def update_variants(variants, template, publish_progress=True): | ||||||
| 	count=0 | 	total = len(variants) | ||||||
| 	for d in variants: | 	for count, d in enumerate(variants, start=1): | ||||||
| 		variant = frappe.get_doc("Item", d) | 		variant = frappe.get_doc("Item", d) | ||||||
| 		copy_attributes_to_variant(template, variant) | 		copy_attributes_to_variant(template, variant) | ||||||
| 		variant.save() | 		variant.save() | ||||||
| 		count+=1 |  | ||||||
| 		if publish_progress: | 		if publish_progress: | ||||||
| 				frappe.publish_progress(count*100/len(variants), title = _("Updating Variants...")) | 			frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) | ||||||
| 
 | 
 | ||||||
| def on_doctype_update(): | def on_doctype_update(): | ||||||
| 	# since route is a Text column, it needs a length for indexing | 	# since route is a Text column, it needs a length for indexing | ||||||
|  | |||||||
| @ -10,14 +10,15 @@ from frappe.test_runner import make_test_objects | |||||||
| from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, | from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, | ||||||
| 	InvalidItemAttributeValueError, get_variant) | 	InvalidItemAttributeValueError, get_variant) | ||||||
| from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode | from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode | ||||||
| from erpnext.stock.doctype.item.item import get_uom_conv_factor | from erpnext.stock.doctype.item.item import (get_uom_conv_factor, get_item_attribute, | ||||||
|  | 	validate_is_stock_item, get_timeline_data) | ||||||
| from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry | from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry | ||||||
| from erpnext.stock.get_item_details import get_item_details | from erpnext.stock.get_item_details import get_item_details | ||||||
|  | from erpnext.tests.utils import change_settings | ||||||
| 
 | 
 | ||||||
| from six import iteritems |  | ||||||
| 
 | 
 | ||||||
| test_ignore = ["BOM"] | test_ignore = ["BOM"] | ||||||
| test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand"] | test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] | ||||||
| 
 | 
 | ||||||
| def make_item(item_code, properties=None): | def make_item(item_code, properties=None): | ||||||
| 	if frappe.db.exists("Item", item_code): | 	if frappe.db.exists("Item", item_code): | ||||||
| @ -98,7 +99,7 @@ class TestItem(unittest.TestCase): | |||||||
| 			"ignore_pricing_rule": 1 | 			"ignore_pricing_rule": 1 | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		for key, value in iteritems(to_check): | 		for key, value in to_check.items(): | ||||||
| 			self.assertEqual(value, details.get(key)) | 			self.assertEqual(value, details.get(key)) | ||||||
| 
 | 
 | ||||||
| 	def test_item_tax_template(self): | 	def test_item_tax_template(self): | ||||||
| @ -194,7 +195,7 @@ class TestItem(unittest.TestCase): | |||||||
| 			"plc_conversion_rate": 1, | 			"plc_conversion_rate": 1, | ||||||
| 			"customer": "_Test Customer", | 			"customer": "_Test Customer", | ||||||
| 		}) | 		}) | ||||||
| 		for key, value in iteritems(sales_item_check): | 		for key, value in sales_item_check.items(): | ||||||
| 			self.assertEqual(value, sales_item_details.get(key)) | 			self.assertEqual(value, sales_item_details.get(key)) | ||||||
| 
 | 
 | ||||||
| 		purchase_item_check = { | 		purchase_item_check = { | ||||||
| @ -215,7 +216,7 @@ class TestItem(unittest.TestCase): | |||||||
| 			"plc_conversion_rate": 1, | 			"plc_conversion_rate": 1, | ||||||
| 			"supplier": "_Test Supplier", | 			"supplier": "_Test Supplier", | ||||||
| 		}) | 		}) | ||||||
| 		for key, value in iteritems(purchase_item_check): | 		for key, value in purchase_item_check.items(): | ||||||
| 			self.assertEqual(value, purchase_item_details.get(key)) | 			self.assertEqual(value, purchase_item_details.get(key)) | ||||||
| 
 | 
 | ||||||
| 	def test_item_attribute_change_after_variant(self): | 	def test_item_attribute_change_after_variant(self): | ||||||
| @ -375,6 +376,14 @@ class TestItem(unittest.TestCase): | |||||||
| 		self.assertEqual(item_doc.uoms[1].uom, "Kg") | 		self.assertEqual(item_doc.uoms[1].uom, "Kg") | ||||||
| 		self.assertEqual(item_doc.uoms[1].conversion_factor, 1000) | 		self.assertEqual(item_doc.uoms[1].conversion_factor, 1000) | ||||||
| 
 | 
 | ||||||
|  | 	def test_uom_conv_intermediate(self): | ||||||
|  | 		factor = get_uom_conv_factor("Pound", "Gram") | ||||||
|  | 		self.assertAlmostEqual(factor, 453.592, 3) | ||||||
|  | 
 | ||||||
|  | 	def test_uom_conv_base_case(self): | ||||||
|  | 		factor = get_uom_conv_factor("m", "m") | ||||||
|  | 		self.assertEqual(factor, 1.0) | ||||||
|  | 
 | ||||||
| 	def test_item_variant_by_manufacturer(self): | 	def test_item_variant_by_manufacturer(self): | ||||||
| 		fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] | 		fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] | ||||||
| 		set_item_variant_settings(fields) | 		set_item_variant_settings(fields) | ||||||
| @ -464,7 +473,7 @@ class TestItem(unittest.TestCase): | |||||||
| 		self.assertEqual(len(matching_barcodes), 1) | 		self.assertEqual(len(matching_barcodes), 1) | ||||||
| 		details = matching_barcodes[0] | 		details = matching_barcodes[0] | ||||||
| 
 | 
 | ||||||
| 		for key, value in iteritems(barcode_properties): | 		for key, value in barcode_properties.items(): | ||||||
| 			self.assertEqual(value, details.get(key)) | 			self.assertEqual(value, details.get(key)) | ||||||
| 
 | 
 | ||||||
| 		# Add barcode again - should cause DuplicateEntryError | 		# Add barcode again - should cause DuplicateEntryError | ||||||
| @ -480,6 +489,89 @@ class TestItem(unittest.TestCase): | |||||||
| 		new_barcode.barcode_type = 'EAN' | 		new_barcode.barcode_type = 'EAN' | ||||||
| 		self.assertRaises(InvalidBarcode, item_doc.save) | 		self.assertRaises(InvalidBarcode, item_doc.save) | ||||||
| 
 | 
 | ||||||
|  | 	def test_heatmap_data(self): | ||||||
|  | 		import time | ||||||
|  | 		data = get_timeline_data("Item", "_Test Item") | ||||||
|  | 		self.assertTrue(isinstance(data, dict)) | ||||||
|  | 
 | ||||||
|  | 		now = time.time() | ||||||
|  | 		one_year_ago = now - 366 * 24 * 60 * 60 | ||||||
|  | 
 | ||||||
|  | 		for timestamp, count in data.items(): | ||||||
|  | 			self.assertIsInstance(timestamp, int) | ||||||
|  | 			self.assertTrue(one_year_ago <= timestamp <= now) | ||||||
|  | 			self.assertIsInstance(count, int) | ||||||
|  | 			self.assertTrue(count >= 0) | ||||||
|  | 
 | ||||||
|  | 	def test_index_creation(self): | ||||||
|  | 		"check if index is getting created in db" | ||||||
|  | 		from erpnext.stock.doctype.item.item import on_doctype_update | ||||||
|  | 		on_doctype_update() | ||||||
|  | 
 | ||||||
|  | 		indices = frappe.db.sql("show index from tabItem", as_dict=1) | ||||||
|  | 		expected_columns = {"item_code", "item_name", "item_group", "route"} | ||||||
|  | 		for index in indices: | ||||||
|  | 			expected_columns.discard(index.get("Column_name")) | ||||||
|  | 
 | ||||||
|  | 		if expected_columns: | ||||||
|  | 			self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}") | ||||||
|  | 
 | ||||||
|  | 	def test_attribute_completions(self): | ||||||
|  | 		expected_attrs = {"Small", "Extra Small", "Extra Large", "Large", "2XL", "Medium"} | ||||||
|  | 
 | ||||||
|  | 		attrs = get_item_attribute("Test Size") | ||||||
|  | 		received_attrs = {attr.attribute_value for attr in attrs} | ||||||
|  | 		self.assertEqual(received_attrs, expected_attrs) | ||||||
|  | 
 | ||||||
|  | 		attrs = get_item_attribute("Test Size", attribute_value="extra") | ||||||
|  | 		received_attrs = {attr.attribute_value for attr in attrs} | ||||||
|  | 		self.assertEqual(received_attrs, {"Extra Small", "Extra Large"}) | ||||||
|  | 
 | ||||||
|  | 	def test_check_stock_uom_with_bin(self): | ||||||
|  | 		# this item has opening stock and stock_uom set in test_records. | ||||||
|  | 		item = frappe.get_doc("Item", "_Test Item") | ||||||
|  | 		item.stock_uom = "Gram" | ||||||
|  | 		self.assertRaises(frappe.ValidationError, item.save) | ||||||
|  | 
 | ||||||
|  | 	def test_check_stock_uom_with_bin_no_sle(self): | ||||||
|  | 		from erpnext.stock.stock_balance import update_bin_qty | ||||||
|  | 		item = create_item("_Item with bin qty") | ||||||
|  | 		item.stock_uom = "Gram" | ||||||
|  | 		item.save() | ||||||
|  | 
 | ||||||
|  | 		update_bin_qty(item.item_code, "_Test Warehouse - _TC", { | ||||||
|  | 			"reserved_qty": 10 | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		item.stock_uom = "Kilometer" | ||||||
|  | 		self.assertRaises(frappe.ValidationError, item.save) | ||||||
|  | 
 | ||||||
|  | 		update_bin_qty(item.item_code, "_Test Warehouse - _TC", { | ||||||
|  | 			"reserved_qty": 0 | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		item.load_from_db() | ||||||
|  | 		item.stock_uom = "Kilometer" | ||||||
|  | 		try: | ||||||
|  | 			item.save() | ||||||
|  | 		except frappe.ValidationError as e: | ||||||
|  | 			self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}") | ||||||
|  | 
 | ||||||
|  | 	def test_validate_stock_item(self): | ||||||
|  | 		self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item") | ||||||
|  | 
 | ||||||
|  | 		try: | ||||||
|  | 			validate_is_stock_item("_Test Item") | ||||||
|  | 		except frappe.ValidationError as e: | ||||||
|  | 			self.fail(f"stock item considered non-stock item: {e}") | ||||||
|  | 
 | ||||||
|  | 	@change_settings("Stock Settings", {"item_naming_by": "Naming Series"}) | ||||||
|  | 	def test_autoname_series(self): | ||||||
|  | 		item = frappe.new_doc("Item") | ||||||
|  | 		item.item_group = "All Item Groups" | ||||||
|  | 		item.save()  # if item code saved without item_code then series worked | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def set_item_variant_settings(fields): | def set_item_variant_settings(fields): | ||||||
| 	doc = frappe.get_doc('Item Variant Settings') | 	doc = frappe.get_doc('Item Variant Settings') | ||||||
| 	doc.set('fields', fields) | 	doc.set('fields', fields) | ||||||
| @ -494,23 +586,24 @@ def make_item_variant(): | |||||||
| 
 | 
 | ||||||
| test_records = frappe.get_test_records('Item') | test_records = frappe.get_test_records('Item') | ||||||
| 
 | 
 | ||||||
| def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, | def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", | ||||||
| 	customer=None, is_purchase_item=None, opening_stock=None, company=None): | 		is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, | ||||||
|  | 		company="_Test Company"): | ||||||
| 	if not frappe.db.exists("Item", item_code): | 	if not frappe.db.exists("Item", item_code): | ||||||
| 		item = frappe.new_doc("Item") | 		item = frappe.new_doc("Item") | ||||||
| 		item.item_code = item_code | 		item.item_code = item_code | ||||||
| 		item.item_name = item_code | 		item.item_name = item_code | ||||||
| 		item.description = item_code | 		item.description = item_code | ||||||
| 		item.item_group = "All Item Groups" | 		item.item_group = "All Item Groups" | ||||||
| 		item.is_stock_item = is_stock_item or 1 | 		item.is_stock_item = is_stock_item | ||||||
| 		item.opening_stock = opening_stock or 0 | 		item.opening_stock = opening_stock | ||||||
| 		item.valuation_rate = valuation_rate or 0.0 | 		item.valuation_rate = valuation_rate | ||||||
| 		item.is_purchase_item = is_purchase_item | 		item.is_purchase_item = is_purchase_item | ||||||
| 		item.is_customer_provided_item = is_customer_provided_item | 		item.is_customer_provided_item = is_customer_provided_item | ||||||
| 		item.customer = customer or '' | 		item.customer = customer or '' | ||||||
| 		item.append("item_defaults", { | 		item.append("item_defaults", { | ||||||
| 			"default_warehouse": warehouse or '_Test Warehouse - _TC', | 			"default_warehouse": warehouse, | ||||||
| 			"company": company or "_Test Company" | 			"company": company | ||||||
| 		}) | 		}) | ||||||
| 		item.save() | 		item.save() | ||||||
| 	else: | 	else: | ||||||
|  | |||||||
| @ -1,29 +1,45 @@ | |||||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors | ||||||
| # See license.txt | # See license.txt | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals |  | ||||||
| import frappe |  | ||||||
| import unittest | import unittest | ||||||
|  | 
 | ||||||
|  | import frappe | ||||||
| from frappe.utils import nowdate | from frappe.utils import nowdate | ||||||
| from erpnext.stock.doctype.item.test_item import create_item | 
 | ||||||
|  | from erpnext.controllers.stock_controller import ( | ||||||
|  | 	QualityInspectionNotSubmittedError, | ||||||
|  | 	QualityInspectionRejectedError, | ||||||
|  | 	QualityInspectionRequiredError, | ||||||
|  | 	make_quality_inspections, | ||||||
|  | ) | ||||||
| from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note | from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note | ||||||
|  | from erpnext.stock.doctype.item.test_item import create_item | ||||||
| from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry | from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry | ||||||
| from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError |  | ||||||
| 
 | 
 | ||||||
| # test_records = frappe.get_test_records('Quality Inspection') | # test_records = frappe.get_test_records('Quality Inspection') | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class TestQualityInspection(unittest.TestCase): | class TestQualityInspection(unittest.TestCase): | ||||||
| 	def setUp(self): | 	def setUp(self): | ||||||
| 		create_item("_Test Item with QA") | 		create_item("_Test Item with QA") | ||||||
| 		frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1) | 		frappe.db.set_value( | ||||||
|  | 			"Item", "_Test Item with QA", "inspection_required_before_delivery", 1 | ||||||
|  | 		) | ||||||
| 
 | 
 | ||||||
| 	def test_qa_for_delivery(self): | 	def test_qa_for_delivery(self): | ||||||
| 		make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100) | 		make_stock_entry( | ||||||
|  | 			item_code="_Test Item with QA", | ||||||
|  | 			target="_Test Warehouse - _TC", | ||||||
|  | 			qty=1, | ||||||
|  | 			basic_rate=100 | ||||||
|  | 		) | ||||||
| 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | ||||||
| 
 | 
 | ||||||
| 		self.assertRaises(QualityInspectionRequiredError, dn.submit) | 		self.assertRaises(QualityInspectionRequiredError, dn.submit) | ||||||
| 
 | 
 | ||||||
| 		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected") | 		qa = create_quality_inspection( | ||||||
|  | 			reference_type="Delivery Note", reference_name=dn.name, status="Rejected" | ||||||
|  | 		) | ||||||
| 		dn.reload() | 		dn.reload() | ||||||
| 		self.assertRaises(QualityInspectionRejectedError, dn.submit) | 		self.assertRaises(QualityInspectionRejectedError, dn.submit) | ||||||
| 
 | 
 | ||||||
| @ -38,7 +54,9 @@ class TestQualityInspection(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 	def test_qa_not_submit(self): | 	def test_qa_not_submit(self): | ||||||
| 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | ||||||
| 		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True) | 		qa = create_quality_inspection( | ||||||
|  | 			reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True | ||||||
|  | 		) | ||||||
| 		dn.items[0].quality_inspection = qa.name | 		dn.items[0].quality_inspection = qa.name | ||||||
| 		self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) | 		self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) | ||||||
| 
 | 
 | ||||||
| @ -48,21 +66,28 @@ class TestQualityInspection(unittest.TestCase): | |||||||
| 	def test_value_based_qi_readings(self): | 	def test_value_based_qi_readings(self): | ||||||
| 		# Test QI based on acceptance values (Non formula) | 		# Test QI based on acceptance values (Non formula) | ||||||
| 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | ||||||
| 		readings = [{ | 		readings = [ | ||||||
| 			"specification": "Iron Content", # numeric reading | 			{ | ||||||
| 			"min_value": 0.1, | 				"specification": "Iron Content",  # numeric reading | ||||||
| 			"max_value": 0.9, | 				"min_value": 0.1, | ||||||
| 			"reading_1": "0.4" | 				"max_value": 0.9, | ||||||
| 		}, | 				"reading_1": "0.4" | ||||||
| 		{ | 			}, | ||||||
| 			"specification": "Particle Inspection Needed", # non-numeric reading | 			{ | ||||||
| 			"numeric": 0, | 				"specification": "Particle Inspection Needed",  # non-numeric reading | ||||||
| 			"value": "Yes", | 				"numeric": 0, | ||||||
| 			"reading_value": "Yes" | 				"value": "Yes", | ||||||
| 		}] | 				"reading_value": "Yes" | ||||||
|  | 			} | ||||||
|  | 		] | ||||||
|  | 
 | ||||||
|  | 		qa = create_quality_inspection( | ||||||
|  | 			reference_type="Delivery Note", | ||||||
|  | 			reference_name=dn.name, | ||||||
|  | 			readings=readings, | ||||||
|  | 			do_not_save=True | ||||||
|  | 		) | ||||||
| 
 | 
 | ||||||
| 		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, |  | ||||||
| 			readings=readings, do_not_save=True) |  | ||||||
| 		qa.save() | 		qa.save() | ||||||
| 
 | 
 | ||||||
| 		# status must be auto set as per formula | 		# status must be auto set as per formula | ||||||
| @ -74,36 +99,43 @@ class TestQualityInspection(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 	def test_formula_based_qi_readings(self): | 	def test_formula_based_qi_readings(self): | ||||||
| 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | ||||||
| 		readings = [{ | 		readings = [ | ||||||
| 			"specification": "Iron Content", # numeric reading | 			{ | ||||||
| 			"formula_based_criteria": 1, | 				"specification": "Iron Content",  # numeric reading | ||||||
| 			"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", | 				"formula_based_criteria": 1, | ||||||
| 			"reading_1": "0.4" | 				"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", | ||||||
| 		}, | 				"reading_1": "0.4" | ||||||
| 		{ | 			}, | ||||||
| 			"specification": "Calcium Content", # numeric reading | 			{ | ||||||
| 			"formula_based_criteria": 1, | 				"specification": "Calcium Content",  # numeric reading | ||||||
| 			"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", | 				"formula_based_criteria": 1, | ||||||
| 			"reading_1": "0.7" | 				"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", | ||||||
| 		}, | 				"reading_1": "0.7" | ||||||
| 		{ | 			}, | ||||||
| 			"specification": "Mg Content", # numeric reading | 			{ | ||||||
| 			"formula_based_criteria": 1, | 				"specification": "Mg Content",  # numeric reading | ||||||
| 			"acceptance_formula": "mean < 0.9", | 				"formula_based_criteria": 1, | ||||||
| 			"reading_1": "0.5", | 				"acceptance_formula": "mean < 0.9", | ||||||
| 			"reading_2": "0.7", | 				"reading_1": "0.5", | ||||||
| 			"reading_3": "random text" # check if random string input causes issues | 				"reading_2": "0.7", | ||||||
| 		}, | 				"reading_3": "random text"  # check if random string input causes issues | ||||||
| 		{ | 			}, | ||||||
| 			"specification": "Calcium Content", # non-numeric reading | 			{ | ||||||
| 			"formula_based_criteria": 1, | 				"specification": "Calcium Content",  # non-numeric reading | ||||||
| 			"numeric": 0, | 				"formula_based_criteria": 1, | ||||||
| 			"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", | 				"numeric": 0, | ||||||
| 			"reading_value": "Grade B" | 				"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", | ||||||
| 		}] | 				"reading_value": "Grade B" | ||||||
|  | 			} | ||||||
|  | 		] | ||||||
|  | 
 | ||||||
|  | 		qa = create_quality_inspection( | ||||||
|  | 			reference_type="Delivery Note", | ||||||
|  | 			reference_name=dn.name, | ||||||
|  | 			readings=readings, | ||||||
|  | 			do_not_save=True | ||||||
|  | 		) | ||||||
| 
 | 
 | ||||||
| 		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, |  | ||||||
| 			readings=readings, do_not_save=True) |  | ||||||
| 		qa.save() | 		qa.save() | ||||||
| 
 | 
 | ||||||
| 		# status must be auto set as per formula | 		# status must be auto set as per formula | ||||||
| @ -115,6 +147,19 @@ class TestQualityInspection(unittest.TestCase): | |||||||
| 		qa.delete() | 		qa.delete() | ||||||
| 		dn.delete() | 		dn.delete() | ||||||
| 
 | 
 | ||||||
|  | 	def test_make_quality_inspections_from_linked_document(self): | ||||||
|  | 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) | ||||||
|  | 		for item in dn.items: | ||||||
|  | 			item.sample_size = item.qty | ||||||
|  | 		quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items) | ||||||
|  | 		self.assertEqual(len(dn.items), len(quality_inspections)) | ||||||
|  | 
 | ||||||
|  | 		# cleanup | ||||||
|  | 		for qi in quality_inspections: | ||||||
|  | 			frappe.delete_doc("Quality Inspection", qi) | ||||||
|  | 		dn.delete() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def create_quality_inspection(**args): | def create_quality_inspection(**args): | ||||||
| 	args = frappe._dict(args) | 	args = frappe._dict(args) | ||||||
| 	qa = frappe.new_doc("Quality Inspection") | 	qa = frappe.new_doc("Quality Inspection") | ||||||
| @ -134,7 +179,7 @@ def create_quality_inspection(**args): | |||||||
| 		readings = args.readings | 		readings = args.readings | ||||||
| 
 | 
 | ||||||
| 	if args.status == "Rejected": | 	if args.status == "Rejected": | ||||||
| 		readings["reading_1"] = "12" # status is auto set in child on save | 		readings["reading_1"] = "12"  # status is auto set in child on save | ||||||
| 
 | 
 | ||||||
| 	if isinstance(readings, list): | 	if isinstance(readings, list): | ||||||
| 		for entry in readings: | 		for entry in readings: | ||||||
| @ -150,10 +195,11 @@ def create_quality_inspection(**args): | |||||||
| 
 | 
 | ||||||
| 	return qa | 	return qa | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def create_quality_inspection_parameter(parameter): | def create_quality_inspection_parameter(parameter): | ||||||
| 	if not frappe.db.exists("Quality Inspection Parameter", parameter): | 	if not frappe.db.exists("Quality Inspection Parameter", parameter): | ||||||
| 		frappe.get_doc({ | 		frappe.get_doc({ | ||||||
| 			"doctype": "Quality Inspection Parameter", | 			"doctype": "Quality Inspection Parameter", | ||||||
| 			"parameter": parameter, | 			"parameter": parameter, | ||||||
| 			"description": parameter | 			"description": parameter | ||||||
| 		}).insert() | 		}).insert() | ||||||
|  | |||||||
| @ -115,6 +115,14 @@ frappe.ui.form.on('Stock Entry', { | |||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if (!frm.is_new() && frm.doc.docstatus === 0) { | ||||||
|  | 			frm.add_custom_button(__("Quality Inspection(s)"), () => { | ||||||
|  | 				let transaction_controller = new erpnext.TransactionController({ frm: frm }); | ||||||
|  | 				transaction_controller.make_quality_inspection(); | ||||||
|  | 			}, __("Create")); | ||||||
|  | 			frm.page.set_inner_btn_group_as_primary(__('Create')); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); | 		let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); | ||||||
| 		quality_inspection_field.get_route_options_for_new_doc = function(row) { | 		quality_inspection_field.get_route_options_for_new_doc = function(row) { | ||||||
| 			if (frm.is_new()) return; | 			if (frm.is_new()) return; | ||||||
| @ -155,7 +163,7 @@ frappe.ui.form.on('Stock Entry', { | |||||||
| 	refresh: function(frm) { | 	refresh: function(frm) { | ||||||
| 		if(!frm.doc.docstatus) { | 		if(!frm.doc.docstatus) { | ||||||
| 			frm.trigger('validate_purpose_consumption'); | 			frm.trigger('validate_purpose_consumption'); | ||||||
| 			frm.add_custom_button(__('Create Material Request'), function() { | 			frm.add_custom_button(__('Material Request'), function() { | ||||||
| 				frappe.model.with_doctype('Material Request', function() { | 				frappe.model.with_doctype('Material Request', function() { | ||||||
| 					var mr = frappe.model.get_new_doc('Material Request'); | 					var mr = frappe.model.get_new_doc('Material Request'); | ||||||
| 					var items = frm.get_field('items').grid.get_selected_children(); | 					var items = frm.get_field('items').grid.get_selected_children(); | ||||||
| @ -178,7 +186,7 @@ frappe.ui.form.on('Stock Entry', { | |||||||
| 					}); | 					}); | ||||||
| 					frappe.set_route('Form', 'Material Request', mr.name); | 					frappe.set_route('Form', 'Material Request', mr.name); | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}, __("Create")); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if(frm.doc.items) { | 		if(frm.doc.items) { | ||||||
|  | |||||||
| @ -96,7 +96,7 @@ class StockReconciliation(StockController): | |||||||
| 
 | 
 | ||||||
| 	def validate_data(self): | 	def validate_data(self): | ||||||
| 		def _get_msg(row_num, msg): | 		def _get_msg(row_num, msg): | ||||||
| 			return _("Row # {0}: ").format(row_num+1) + msg | 			return _("Row # {0}:").format(row_num+1) + " " + msg | ||||||
| 
 | 
 | ||||||
| 		self.validation_messages = [] | 		self.validation_messages = [] | ||||||
| 		item_warehouse_combinations = [] | 		item_warehouse_combinations = [] | ||||||
| @ -167,8 +167,8 @@ class StockReconciliation(StockController): | |||||||
| 			item = frappe.get_doc("Item", item_code) | 			item = frappe.get_doc("Item", item_code) | ||||||
| 
 | 
 | ||||||
| 			# end of life and stock item | 			# end of life and stock item | ||||||
| 			validate_end_of_life(item_code, item.end_of_life, item.disabled, verbose=0) | 			validate_end_of_life(item_code, item.end_of_life, item.disabled) | ||||||
| 			validate_is_stock_item(item_code, item.is_stock_item, verbose=0) | 			validate_is_stock_item(item_code, item.is_stock_item) | ||||||
| 
 | 
 | ||||||
| 			# item should not be serialized | 			# item should not be serialized | ||||||
| 			if item.has_serial_no and not row.serial_no and not item.serial_no_series: | 			if item.has_serial_no and not row.serial_no and not item.serial_no_series: | ||||||
| @ -179,10 +179,10 @@ class StockReconciliation(StockController): | |||||||
| 				raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) | 				raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) | ||||||
| 
 | 
 | ||||||
| 			# docstatus should be < 2 | 			# docstatus should be < 2 | ||||||
| 			validate_cancelled_item(item_code, item.docstatus, verbose=0) | 			validate_cancelled_item(item_code, item.docstatus) | ||||||
| 
 | 
 | ||||||
| 		except Exception as e: | 		except Exception as e: | ||||||
| 			self.validation_messages.append(_("Row # ") + ("%d: " % (row.idx)) + cstr(e)) | 			self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) | ||||||
| 
 | 
 | ||||||
| 	def update_stock_ledger(self): | 	def update_stock_ledger(self): | ||||||
| 		"""	find difference between current and expected entries | 		"""	find difference between current and expected entries | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | ||||||
| # License: GNU General Public License v3. See license.txt | # License: GNU General Public License v3. See license.txt | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals | import copy | ||||||
|  | from contextlib import contextmanager | ||||||
| 
 | 
 | ||||||
| import frappe | import frappe | ||||||
| 
 | 
 | ||||||
| @ -41,3 +42,38 @@ def create_test_contact_and_address(): | |||||||
| 	contact.add_email("test_contact_customer@example.com", is_primary=True) | 	contact.add_email("test_contact_customer@example.com", is_primary=True) | ||||||
| 	contact.add_phone("+91 0000000000", is_primary_phone=True) | 	contact.add_phone("+91 0000000000", is_primary_phone=True) | ||||||
| 	contact.insert() | 	contact.insert() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @contextmanager | ||||||
|  | def change_settings(doctype, settings_dict): | ||||||
|  | 	""" A context manager to ensure that settings are changed before running | ||||||
|  | 	function and restored after running it regardless of exceptions occured. | ||||||
|  | 	This is useful in tests where you want to make changes in a function but | ||||||
|  | 	don't retain those changes. | ||||||
|  | 	import and use as decorator to cover full function or using `with` statement. | ||||||
|  | 
 | ||||||
|  | 	example: | ||||||
|  | 	@change_settings("Stock Settings", {"item_naming_by": "Naming Series"}) | ||||||
|  | 	def test_case(self): | ||||||
|  | 		... | ||||||
|  | 	""" | ||||||
|  | 
 | ||||||
|  | 	try: | ||||||
|  | 		settings = frappe.get_doc(doctype) | ||||||
|  | 		# remember setting | ||||||
|  | 		previous_settings = copy.deepcopy(settings_dict) | ||||||
|  | 		for key in previous_settings: | ||||||
|  | 			previous_settings[key] = getattr(settings, key) | ||||||
|  | 
 | ||||||
|  | 		# change setting | ||||||
|  | 		for key, value in settings_dict.items(): | ||||||
|  | 			setattr(settings, key, value) | ||||||
|  | 		settings.save() | ||||||
|  | 		yield # yield control to calling function | ||||||
|  | 
 | ||||||
|  | 	finally: | ||||||
|  | 		# restore settings | ||||||
|  | 		settings = frappe.get_doc(doctype) | ||||||
|  | 		for key, value in previous_settings.items(): | ||||||
|  | 			setattr(settings, key, value) | ||||||
|  | 		settings.save() | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user