Merge pull request #18527 from netchampfaris/bom-comparison-tool
feat: BOM Comparison Tool
This commit is contained in:
commit
7638301d01
@ -94,6 +94,13 @@ def get_data():
|
||||
"name": "BOM Update Tool",
|
||||
"description": _("Replace BOM and update latest price in all BOMs"),
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("BOM Comparison Tool"),
|
||||
"name": "bom-comparison-tool",
|
||||
"description": _("Compare BOMs for changes in Raw Materials and Operations"),
|
||||
"data_doctype": "BOM"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
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 @@ def add_additional_cost(stock_entry, work_order):
|
||||
'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
|
||||
|
@ -0,0 +1,213 @@
|
||||
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,, 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;
|
||||
let th = i === 0
|
||||
? `<th rowspan="${values_changed.length}">${item_code}</th>`
|
||||
: '';
|
||||
return `
|
||||
<tr>
|
||||
${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;
|
||||
let cells = fields
|
||||
.map(df => `<td>${doc[df.fieldname]}</td>`)
|
||||
.join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
|
||||
let header = fields.map(df => `<th>${df.label}</th>`).join('');
|
||||
return `
|
||||
<h4 class="margin-top">${$.format(title, [df.label])}</h4>
|
||||
<table class="table table-bordered">
|
||||
<tr>${header}</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;
|
||||
}, {});
|
||||
}
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user