Merge pull request #18527 from netchampfaris/bom-comparison-tool

feat: BOM Comparison Tool
This commit is contained in:
Deepesh Garg 2019-08-05 11:58:46 +05:30 committed by GitHub
commit 7638301d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 759 additions and 1999 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}, {});
}

View File

@ -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"
}