feat: BOM Comparison Tool
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a716293..22c2f69 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -9,6 +9,7 @@
 from frappe.website.website_generator import WebsiteGenerator
 from erpnext.stock.get_item_details import get_conversion_factor
 from erpnext.stock.get_item_details import get_price_list_rate
+from frappe.core.doctype.version.version import get_diff
 
 import functools
 
@@ -763,3 +764,52 @@
 			'description': name[0],
 			'amount': items.get(name[0])
 		})
+
+@frappe.whitelist()
+def get_bom_diff(bom1, bom2):
+	from frappe.model import table_fields
+
+	doc1 = frappe.get_doc('BOM', bom1)
+	doc2 = frappe.get_doc('BOM', bom2)
+
+	out = get_diff(doc1, doc2)
+	out.row_changed = []
+	out.added = []
+	out.removed = []
+
+	meta = doc1.meta
+
+	identifiers = {
+		'operations': 'operation',
+		'items': 'item_code',
+		'scrap_items': 'item_code',
+		'exploded_items': 'item_code'
+	}
+
+	for df in meta.fields:
+		old_value, new_value = doc1.get(df.fieldname), doc2.get(df.fieldname)
+
+		if df.fieldtype in table_fields:
+			identifier = identifiers[df.fieldname]
+			# make maps
+			old_row_by_identifier, new_row_by_identifier = {}, {}
+			for d in old_value:
+				old_row_by_identifier[d.get(identifier)] = d
+			for d in new_value:
+				new_row_by_identifier[d.get(identifier)] = d
+
+			# check rows for additions, changes
+			for i, d in enumerate(new_value):
+				if d.get(identifier) in old_row_by_identifier:
+					diff = get_diff(old_row_by_identifier[d.get(identifier)], d, for_child=True)
+					if diff and diff.changed:
+						out.row_changed.append((df.fieldname, i, d.get(identifier), diff.changed))
+				else:
+					out.added.append([df.fieldname, d.as_dict()])
+
+			# check for deletions
+			for d in old_value:
+				if not d.get(identifier) in new_row_by_identifier:
+					out.removed.append([df.fieldname, d.as_dict()])
+
+	return out
diff --git a/erpnext/manufacturing/page/bom_comparison_tool/__init__.py b/erpnext/manufacturing/page/bom_comparison_tool/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/page/bom_comparison_tool/__init__.py
diff --git a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js
new file mode 100644
index 0000000..eeecdf6
--- /dev/null
+++ b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js
@@ -0,0 +1,218 @@
+frappe.pages['bom-comparison-tool'].on_page_load = function(wrapper) {
+	var page = frappe.ui.make_app_page({
+		parent: wrapper,
+		title: __('BOM Comparison Tool'),
+		single_column: true
+	});
+
+	new erpnext.BOMComparisonTool(page);
+}
+
+erpnext.BOMComparisonTool = class BOMComparisonTool {
+	constructor(page) {
+		this.page = page;
+		this.make_form();
+	}
+
+	make_form() {
+		this.form = new frappe.ui.FieldGroup({
+			fields: [
+				{
+					label: __('BOM 1'),
+					fieldname: 'name1',
+					fieldtype: 'Link',
+					options: 'BOM',
+					change: () => this.fetch_and_render()
+				},
+				{
+					fieldtype: 'Column Break'
+				},
+				{
+					label: __('BOM 2'),
+					fieldname: 'name2',
+					fieldtype: 'Link',
+					options: 'BOM',
+					change: () => this.fetch_and_render()
+				},
+				{
+					fieldtype: 'Section Break'
+				},
+				{
+					fieldtype: 'HTML',
+					fieldname: 'preview'
+				}
+			],
+			body: this.page.body
+		});
+		this.form.make();
+	}
+
+	fetch_and_render() {
+		let { name1, name2 } = this.form.get_values();
+		if (!(name1 && name2)) {
+			this.form.get_field('preview').html('');
+			return;
+		}
+
+		// set working state
+		this.form.get_field('preview').html(`
+			<div class="text-muted margin-top">
+				${__("Fetching...")}
+			</div>
+		`);
+
+		frappe.call('erpnext.manufacturing.doctype.bom.bom.get_bom_diff', {
+			bom1: name1,
+			bom2: name2
+		}).then(r => {
+			let diff = r.message;
+			frappe.model.with_doctype('BOM', () => {
+				this.render('BOM', name1, name2, diff);
+			});
+		});
+	}
+
+	render(doctype, name1, name2, diff) {
+
+		let change_html = (title, doctype, changed) => {
+			let values_changed = this.get_changed_values(doctype, changed)
+				.map(change => {
+					let [fieldname, value1, value2] = change;
+					return `
+						<tr>
+							<td>${frappe.meta.get_label(doctype, fieldname)}</td>
+							<td>${value1}</td>
+							<td>${value2}</td>
+						</tr>
+					`;
+				})
+				.join('');
+
+			return `
+				<h4 class="margin-top">${title}</h4>
+				<div>
+					<table class="table table-bordered">
+						<tr>
+							<th width="33%">${__('Field')}</th>
+							<th width="33%">${name1}</th>
+							<th width="33%">${name2}</th>
+						</tr>
+						${values_changed}
+					</table>
+				</div>
+			`;
+		}
+
+		let value_changes = change_html(__('Values Changed'), doctype, diff.changed);
+
+		let row_changes_by_fieldname = group_items(diff.row_changed, change => change[0]);
+
+		let table_changes = Object.keys(row_changes_by_fieldname).map(fieldname => {
+			let changes = row_changes_by_fieldname[fieldname];
+			let df = frappe.meta.get_docfield(doctype, fieldname);
+
+			let html = changes.map(change => {
+				let [fieldname, idx, item_code, changes] = change;
+				let df = frappe.meta.get_docfield(doctype, fieldname);
+				let child_doctype = df.options;
+				let values_changed = this.get_changed_values(child_doctype, changes);
+
+				return values_changed.map((change, i) => {
+					let [fieldname, value1, value2] = change;
+					return `
+						<tr>
+							${i === 0
+								? `<th rowspan="${values_changed.length}">${item_code}</th>`
+								: ''}
+							<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
+							<td>${value1}</td>
+							<td>${value2}</td>
+						</tr>
+					`;
+				}).join('');
+			}).join('');
+
+			return `
+				<h4 class="margin-top">${__('Changes in {0}', [df.label])}</h4>
+				<table class="table table-bordered">
+					<tr>
+						<th width="25%">${__('Item Code')}</th>
+						<th width="25%">${__('Field')}</th>
+						<th width="25%">${name1}</th>
+						<th width="25%">${name2}</th>
+					</tr>
+					${html}
+				</table>
+			`;
+		}).join('');
+
+		let get_added_removed_html = (title, grouped_items) => {
+			return Object.keys(grouped_items).map(fieldname => {
+				let rows = grouped_items[fieldname];
+				let df = frappe.meta.get_docfield(doctype, fieldname);
+				let fields = frappe.meta.get_docfields(df.options)
+					.filter(df => df.in_list_view);
+
+				let html = rows.map(row => {
+					let [, doc] = row;
+					return `
+						<tr>
+							<tr>
+							${fields.map(df => {
+								return `<td>${doc[df.fieldname]}</td>`
+							}).join('')}
+						</tr>
+					`;
+				}).join('');
+
+				return `
+					<h4 class="margin-top">${$.format(title, [df.label])}</h4>
+					<table class="table table-bordered">
+						${fields.map(df => {
+							return `<th>${df.label}</th>`
+						}).join('')}
+						</tr>
+						${html}
+					</table>
+				`;
+			}).join('');
+		}
+
+		let added_by_fieldname = group_items(diff.added, change => change[0]);
+		let removed_by_fieldname = group_items(diff.removed, change => change[0]);
+
+		let added_html = get_added_removed_html(__('Rows Added in {0}'), added_by_fieldname);
+		let removed_html = get_added_removed_html(__('Rows Removed in {0}'), removed_by_fieldname);
+
+		let html = `
+			${value_changes}
+			${table_changes}
+			${added_html}
+			${removed_html}
+		`;
+
+		this.form.get_field('preview').html(html);
+	}
+
+	get_changed_values(doctype, changed) {
+		return changed.filter(change => {
+			let [fieldname, value1, value2] = change;
+			if (!value1) value1 = '';
+			if (!value2) value2 = '';
+			if (value1 === value2) return false;
+			let df = frappe.meta.get_docfield(doctype, fieldname);
+			if (!df) return false;
+			if (df.hidden) return false;
+			return true;
+		});
+	}
+}
+
+function group_items(array, fn) {
+	return array.reduce((acc, item) => {
+		let key = fn(item);
+		acc[key] = acc[key] || [];
+		acc[key].push(item);
+		return acc;
+	}, {});
+}
diff --git a/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.json b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.json
new file mode 100644
index 0000000..067a106
--- /dev/null
+++ b/erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.json
@@ -0,0 +1,30 @@
+{
+ "content": null,
+ "creation": "2019-07-29 13:24:38.201981",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2019-07-29 13:24:38.201981",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "bom-comparison-tool",
+ "owner": "Administrator",
+ "page_name": "BOM Comparison Tool",
+ "restrict_to_domain": "Manufacturing",
+ "roles": [
+  {
+   "role": "System Manager"
+  },
+  {
+   "role": "Manufacturing User"
+  },
+  {
+   "role": "Manufacturing Manager"
+  }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "BOM Comparison Tool"
+}
\ No newline at end of file