chore: removal of tally migration feature (#45100)
This commit is contained in:
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.provide("erpnext.tally_migration");
|
||||
|
||||
frappe.ui.form.on("Tally Migration", {
|
||||
onload: function (frm) {
|
||||
let reload_status = true;
|
||||
frappe.realtime.on("tally_migration_progress_update", function (data) {
|
||||
if (reload_status) {
|
||||
frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => {
|
||||
frm.refresh_header();
|
||||
});
|
||||
reload_status = false;
|
||||
}
|
||||
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
|
||||
let error_occurred = data.count === -1;
|
||||
if (data.count == data.total || error_occurred) {
|
||||
window.setTimeout(
|
||||
(title) => {
|
||||
frm.dashboard.hide_progress(title);
|
||||
frm.reload_doc();
|
||||
if (error_occurred) {
|
||||
frappe.msgprint({
|
||||
message: __("An error has occurred during {0}. Check {1} for more details", [
|
||||
repl(
|
||||
"<a href='/app/tally-migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>",
|
||||
{
|
||||
tally_document: frm.docname,
|
||||
}
|
||||
),
|
||||
"<a href='/app/error-log' class='variant-click'>Error Log</a>",
|
||||
]),
|
||||
title: __("Tally Migration Error"),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
},
|
||||
2000,
|
||||
data.title
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("show_logs_preview");
|
||||
erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log);
|
||||
erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log);
|
||||
|
||||
["default_round_off_account", "default_warehouse", "default_cost_center"].forEach((account) => {
|
||||
frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1);
|
||||
frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1);
|
||||
});
|
||||
|
||||
if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
|
||||
if (frm.doc.is_master_data_processed) {
|
||||
if (frm.doc.status != "Importing Master Data") {
|
||||
frm.events.add_button(frm, __("Import Master Data"), "import_master_data");
|
||||
}
|
||||
} else {
|
||||
if (frm.doc.status != "Processing Master Data") {
|
||||
frm.events.add_button(frm, __("Process Master Data"), "process_master_data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) {
|
||||
if (frm.doc.is_day_book_data_processed) {
|
||||
if (frm.doc.status != "Importing Day Book Data") {
|
||||
frm.events.add_button(frm, __("Import Day Book Data"), "import_day_book_data");
|
||||
}
|
||||
} else {
|
||||
if (frm.doc.status != "Processing Day Book Data") {
|
||||
frm.events.add_button(frm, __("Process Day Book Data"), "process_day_book_data");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
erpnext_company: function (frm) {
|
||||
frappe.db.exists("Company", frm.doc.erpnext_company).then((exists) => {
|
||||
if (exists) {
|
||||
frappe.msgprint(
|
||||
__(
|
||||
"Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts",
|
||||
[frm.doc.erpnext_company]
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
add_button: function (frm, label, method) {
|
||||
frm.add_custom_button(label, () => {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: method,
|
||||
freeze: true,
|
||||
});
|
||||
frm.reload_doc();
|
||||
});
|
||||
},
|
||||
|
||||
render_html_table(frm, shown_logs, hidden_logs, field) {
|
||||
if (shown_logs && shown_logs.length > 0) {
|
||||
frm.toggle_display(field, true);
|
||||
} else {
|
||||
frm.toggle_display(field, false);
|
||||
return;
|
||||
}
|
||||
let rows = erpnext.tally_migration.get_html_rows(shown_logs, field);
|
||||
let rows_head, table_caption;
|
||||
|
||||
let table_footer =
|
||||
hidden_logs && hidden_logs.length > 0
|
||||
? `<tr class="text-muted">
|
||||
<td colspan="4">And ${hidden_logs.length} more others</td>
|
||||
</tr>`
|
||||
: "";
|
||||
|
||||
if (field === "fixed_error_log_preview") {
|
||||
rows_head = `<th width="75%">${__("Meta Data")}</th>
|
||||
<th width="10%">${__("Unresolve")}</th>`;
|
||||
table_caption = "Resolved Issues";
|
||||
} else {
|
||||
rows_head = `<th width="75%">${__("Error Message")}</th>
|
||||
<th width="10%">${__("Create")}</th>`;
|
||||
table_caption = "Error Log";
|
||||
}
|
||||
|
||||
frm.get_field(field).$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<caption>${table_caption}</caption>
|
||||
<tr class="text-muted">
|
||||
<th width="5%">${__("#")}</th>
|
||||
<th width="10%">${__("DocType")}</th>
|
||||
${rows_head}
|
||||
</tr>
|
||||
${rows}
|
||||
${table_footer}
|
||||
</table>
|
||||
`);
|
||||
},
|
||||
|
||||
show_error_summary(frm) {
|
||||
let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => {
|
||||
if (row.doc) {
|
||||
if (summary[row.doc.doctype]) {
|
||||
summary[row.doc.doctype] += 1;
|
||||
} else {
|
||||
summary[row.doc.doctype] = 1;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}, {});
|
||||
console.table(summary);
|
||||
},
|
||||
|
||||
show_logs_preview(frm) {
|
||||
let empty = "[]";
|
||||
let import_log = frm.doc.failed_import_log || empty;
|
||||
let completed_log = frm.doc.fixed_errors_log || empty;
|
||||
let render_section = !(import_log === completed_log && import_log === empty);
|
||||
|
||||
frm.toggle_display("import_log_section", render_section);
|
||||
if (render_section) {
|
||||
frm.trigger("show_error_summary");
|
||||
frm.trigger("show_errored_import_log");
|
||||
frm.trigger("show_fixed_errors_log");
|
||||
}
|
||||
},
|
||||
|
||||
show_errored_import_log(frm) {
|
||||
let import_log = erpnext.tally_migration.failed_import_log;
|
||||
let logs = import_log.slice(0, 20);
|
||||
let hidden_logs = import_log.slice(20);
|
||||
|
||||
frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview");
|
||||
},
|
||||
|
||||
show_fixed_errors_log(frm) {
|
||||
let completed_log = erpnext.tally_migration.fixed_errors_log;
|
||||
let logs = completed_log.slice(0, 20);
|
||||
let hidden_logs = completed_log.slice(20);
|
||||
|
||||
frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview");
|
||||
},
|
||||
});
|
||||
|
||||
erpnext.tally_migration.getError = (traceback) => {
|
||||
/* Extracts the Error Message from the Python Traceback or Solved error */
|
||||
let is_multiline = traceback.trim().indexOf("\n") != -1;
|
||||
let message;
|
||||
|
||||
if (is_multiline) {
|
||||
let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1;
|
||||
let error_line = traceback.substr(exc_error_idx);
|
||||
let split_str_idx = error_line.indexOf(":") > 0 ? error_line.indexOf(":") + 1 : 0;
|
||||
message = error_line.slice(split_str_idx).trim();
|
||||
} else {
|
||||
message = traceback;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
erpnext.tally_migration.cleanDoc = (obj) => {
|
||||
/* Strips all null and empty values of your JSON object */
|
||||
let temp = obj;
|
||||
$.each(temp, function (key, value) {
|
||||
if (value === "" || value === null) {
|
||||
delete obj[key];
|
||||
} else if (Object.prototype.toString.call(value) === "[object Object]") {
|
||||
erpnext.tally_migration.cleanDoc(value);
|
||||
} else if ($.isArray(value)) {
|
||||
$.each(value, function (k, v) {
|
||||
erpnext.tally_migration.cleanDoc(v);
|
||||
});
|
||||
}
|
||||
});
|
||||
return temp;
|
||||
};
|
||||
|
||||
erpnext.tally_migration.unresolve = (document) => {
|
||||
/* Mark document migration as unresolved ie. move to failed error log */
|
||||
let frm = cur_frm;
|
||||
let failed_log = erpnext.tally_migration.failed_import_log;
|
||||
let fixed_log = erpnext.tally_migration.fixed_errors_log;
|
||||
|
||||
let modified_fixed_log = fixed_log.filter((row) => {
|
||||
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
|
||||
failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` });
|
||||
|
||||
frm.doc.failed_import_log = JSON.stringify(failed_log);
|
||||
frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log);
|
||||
|
||||
frm.dirty();
|
||||
frm.save();
|
||||
};
|
||||
|
||||
erpnext.tally_migration.resolve = (document) => {
|
||||
/* Mark document migration as resolved ie. move to fixed error log */
|
||||
let frm = cur_frm;
|
||||
let failed_log = erpnext.tally_migration.failed_import_log;
|
||||
let fixed_log = erpnext.tally_migration.fixed_errors_log;
|
||||
|
||||
let modified_failed_log = failed_log.filter((row) => {
|
||||
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
fixed_log.push({ doc: document, exc: `Solved on ${Date()}` });
|
||||
|
||||
frm.doc.failed_import_log = JSON.stringify(modified_failed_log);
|
||||
frm.doc.fixed_errors_log = JSON.stringify(fixed_log);
|
||||
|
||||
frm.dirty();
|
||||
frm.save();
|
||||
};
|
||||
|
||||
erpnext.tally_migration.create_new_doc = (document) => {
|
||||
/* Mark as resolved and create new document */
|
||||
erpnext.tally_migration.resolve(document);
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc",
|
||||
args: {
|
||||
document,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true;
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
erpnext.tally_migration.get_html_rows = (logs, field) => {
|
||||
let index = 0;
|
||||
let rows = logs
|
||||
.map(({ doc, exc }) => {
|
||||
let id = frappe.dom.get_unique_id();
|
||||
let traceback = exc;
|
||||
|
||||
let error_message = erpnext.tally_migration.getError(traceback);
|
||||
index++;
|
||||
|
||||
let show_traceback = `
|
||||
<button class="btn btn-default btn-xs m-3" type="button" data-toggle="collapse" data-target="#${id}-traceback" aria-expanded="false" aria-controls="${id}-traceback">
|
||||
${__("Show Traceback")}
|
||||
</button>
|
||||
<div class="collapse margin-top" id="${id}-traceback">
|
||||
<div class="well">
|
||||
<pre style="font-size: smaller;">${traceback}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let show_doc = `
|
||||
<button class='btn btn-default btn-xs m-3' type='button' data-toggle='collapse' data-target='#${id}-doc' aria-expanded='false' aria-controls='${id}-doc'>
|
||||
${__("Show Document")}
|
||||
</button>
|
||||
<div class="collapse margin-top" id="${id}-doc">
|
||||
<div class="well">
|
||||
<pre style="font-size: smaller;">${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let create_button = `
|
||||
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.create_new_doc(${JSON.stringify(
|
||||
doc
|
||||
)})'>
|
||||
${__("Create Document")}
|
||||
</button>`;
|
||||
|
||||
let mark_as_unresolved = `
|
||||
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.unresolve(${JSON.stringify(
|
||||
doc
|
||||
)})'>
|
||||
${__("Mark as unresolved")}
|
||||
</button>`;
|
||||
|
||||
if (field === "fixed_error_log_preview") {
|
||||
return `<tr>
|
||||
<td>${index}</td>
|
||||
<td>
|
||||
<div>${doc.doctype}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>${error_message}</div>
|
||||
<div>${show_doc}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>${mark_as_unresolved}</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
} else {
|
||||
return `<tr>
|
||||
<td>${index}</td>
|
||||
<td>
|
||||
<div>${doc.doctype}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>${error_message}</div>
|
||||
<div>${show_traceback}</div>
|
||||
<div>${show_doc}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>${create_button}</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
|
||||
return rows;
|
||||
};
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"beta": 1,
|
||||
"creation": "2019-02-01 14:27:09.485238",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"status",
|
||||
"master_data",
|
||||
"is_master_data_processed",
|
||||
"is_master_data_imported",
|
||||
"column_break_2",
|
||||
"tally_creditors_account",
|
||||
"tally_debtors_account",
|
||||
"company_section",
|
||||
"tally_company",
|
||||
"default_uom",
|
||||
"column_break_8",
|
||||
"erpnext_company",
|
||||
"processed_files_section",
|
||||
"chart_of_accounts",
|
||||
"parties",
|
||||
"addresses",
|
||||
"column_break_17",
|
||||
"uoms",
|
||||
"items",
|
||||
"vouchers",
|
||||
"accounts_section",
|
||||
"default_warehouse",
|
||||
"default_round_off_account",
|
||||
"column_break_21",
|
||||
"default_cost_center",
|
||||
"day_book_section",
|
||||
"day_book_data",
|
||||
"column_break_27",
|
||||
"is_day_book_data_processed",
|
||||
"is_day_book_data_imported",
|
||||
"import_log_section",
|
||||
"failed_import_log",
|
||||
"fixed_errors_log",
|
||||
"failed_import_preview",
|
||||
"fixed_error_log_preview"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
|
||||
"fieldname": "master_data",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Master Data"
|
||||
},
|
||||
{
|
||||
"default": "Sundry Creditors",
|
||||
"description": "Creditors Account set in Tally",
|
||||
"fieldname": "tally_creditors_account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Creditors Account",
|
||||
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Sundry Debtors",
|
||||
"description": "Debtors Account set in Tally",
|
||||
"fieldname": "tally_debtors_account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Debtors Account",
|
||||
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_master_data_processed",
|
||||
"fieldname": "company_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Company Name as per Imported Tally Data",
|
||||
"fieldname": "tally_company",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Your Company set in ERPNext",
|
||||
"fieldname": "erpnext_company",
|
||||
"fieldtype": "Data",
|
||||
"label": "ERPNext Company",
|
||||
"read_only_depends_on": "eval:doc.is_master_data_processed==1"
|
||||
},
|
||||
{
|
||||
"fieldname": "processed_files_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Processed Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "chart_of_accounts",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Chart of Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "parties",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Parties"
|
||||
},
|
||||
{
|
||||
"fieldname": "addresses",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Addresses"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "uoms",
|
||||
"fieldtype": "Attach",
|
||||
"label": "UOMs"
|
||||
},
|
||||
{
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "vouchers",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Vouchers"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_master_data_imported",
|
||||
"description": "The accounts are set by the system automatically but do confirm these defaults",
|
||||
"fieldname": "accounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_master_data_processed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Master Data Processed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_day_book_data_processed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Day Book Data Processed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_day_book_data_imported",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Day Book Data Imported",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_master_data_imported",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Master Data Imported",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_master_data_imported",
|
||||
"fieldname": "day_book_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Day Book Data exported from Tally that consists of all historic transactions",
|
||||
"fieldname": "day_book_data",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Day Book Data"
|
||||
},
|
||||
{
|
||||
"default": "Unit",
|
||||
"description": "UOM in case unspecified in imported data",
|
||||
"fieldname": "default_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default UOM",
|
||||
"options": "UOM",
|
||||
"read_only_depends_on": "eval:doc.is_master_data_imported==1"
|
||||
},
|
||||
{
|
||||
"default": "[]",
|
||||
"fieldname": "failed_import_log",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "failed_import_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Failed Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_round_off_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Round Off Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "[]",
|
||||
"fieldname": "fixed_errors_log",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "fixed_error_log_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Fixed Error Log"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:51.146772",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Tally Migration",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,768 +0,0 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import zipfile
|
||||
from decimal import Decimal
|
||||
|
||||
import frappe
|
||||
from bs4 import BeautifulSoup as bs
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import (
|
||||
create_custom_fields as _create_custom_fields,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import format_datetime
|
||||
|
||||
from erpnext import encode_company_abbr
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
|
||||
from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import (
|
||||
unset_existing_data,
|
||||
)
|
||||
|
||||
PRIMARY_ACCOUNT = "Primary"
|
||||
VOUCHER_CHUNK_SIZE = 500
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def new_doc(document):
|
||||
document = json.loads(document)
|
||||
doctype = document.pop("doctype")
|
||||
document.pop("name", None)
|
||||
doc = frappe.new_doc(doctype)
|
||||
doc.update(document)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class TallyMigration(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
addresses: DF.Attach | None
|
||||
chart_of_accounts: DF.Attach | None
|
||||
day_book_data: DF.Attach | None
|
||||
default_cost_center: DF.Link | None
|
||||
default_round_off_account: DF.Link | None
|
||||
default_uom: DF.Link | None
|
||||
default_warehouse: DF.Link | None
|
||||
erpnext_company: DF.Data | None
|
||||
failed_import_log: DF.Code | None
|
||||
fixed_errors_log: DF.Code | None
|
||||
is_day_book_data_imported: DF.Check
|
||||
is_day_book_data_processed: DF.Check
|
||||
is_master_data_imported: DF.Check
|
||||
is_master_data_processed: DF.Check
|
||||
items: DF.Attach | None
|
||||
master_data: DF.Attach | None
|
||||
parties: DF.Attach | None
|
||||
status: DF.Data | None
|
||||
tally_company: DF.Data | None
|
||||
tally_creditors_account: DF.Data
|
||||
tally_debtors_account: DF.Data
|
||||
uoms: DF.Attach | None
|
||||
vouchers: DF.Attach | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
failed_import_log = json.loads(self.failed_import_log)
|
||||
sorted_failed_import_log = sorted(failed_import_log, key=lambda row: row["doc"]["creation"])
|
||||
self.failed_import_log = json.dumps(sorted_failed_import_log)
|
||||
|
||||
def autoname(self):
|
||||
if not self.name:
|
||||
self.name = "Tally Migration on " + format_datetime(self.creation)
|
||||
|
||||
def get_collection(self, data_file):
|
||||
def sanitize(string):
|
||||
return re.sub("", "", string)
|
||||
|
||||
def emptify(string):
|
||||
string = re.sub(r"<\w+/>", "", string)
|
||||
string = re.sub(r"<([\w.]+)>\s*<\/\1>", "", string)
|
||||
string = re.sub(r"\r\n", "", string)
|
||||
return string
|
||||
|
||||
master_file = frappe.get_doc("File", {"file_url": data_file})
|
||||
master_file_path = master_file.get_full_path()
|
||||
|
||||
if zipfile.is_zipfile(master_file_path):
|
||||
with zipfile.ZipFile(master_file_path) as zf:
|
||||
encoded_content = zf.read(zf.namelist()[0])
|
||||
try:
|
||||
content = encoded_content.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
content = encoded_content.decode("utf-16")
|
||||
|
||||
master = bs(sanitize(emptify(content)), "xml")
|
||||
collection = master.BODY.IMPORTDATA.REQUESTDATA
|
||||
return collection
|
||||
|
||||
def dump_processed_data(self, data):
|
||||
for key, value in data.items():
|
||||
f = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": key + ".json",
|
||||
"attached_to_doctype": self.doctype,
|
||||
"attached_to_name": self.name,
|
||||
"content": json.dumps(value),
|
||||
"is_private": True,
|
||||
}
|
||||
)
|
||||
try:
|
||||
f.insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
setattr(self, key, f.file_url)
|
||||
|
||||
def set_account_defaults(self):
|
||||
self.default_cost_center, self.default_round_off_account = frappe.db.get_value(
|
||||
"Company", self.erpnext_company, ["cost_center", "round_off_account"]
|
||||
)
|
||||
self.default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||
|
||||
def _process_master_data(self):
|
||||
def get_company_name(collection):
|
||||
return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip()
|
||||
|
||||
def get_coa_customers_suppliers(collection):
|
||||
root_type_map = {
|
||||
"Application of Funds (Assets)": "Asset",
|
||||
"Expenses": "Expense",
|
||||
"Income": "Income",
|
||||
"Source of Funds (Liabilities)": "Liability",
|
||||
}
|
||||
roots = set(root_type_map.keys())
|
||||
accounts = list(get_groups(collection.find_all("GROUP"))) + list(
|
||||
get_ledgers(collection.find_all("LEDGER"))
|
||||
)
|
||||
children, parents = get_children_and_parent_dict(accounts)
|
||||
group_set = [acc[1] for acc in accounts if acc[2]]
|
||||
children, customers, suppliers = remove_parties(parents, children, group_set)
|
||||
|
||||
try:
|
||||
coa = traverse({}, children, roots, roots, group_set)
|
||||
except RecursionError:
|
||||
self.log(
|
||||
_(
|
||||
"Error occurred while parsing Chart of Accounts: Please make sure that no two accounts have the same name"
|
||||
)
|
||||
)
|
||||
|
||||
for account in coa:
|
||||
coa[account]["root_type"] = root_type_map[account]
|
||||
|
||||
return coa, customers, suppliers
|
||||
|
||||
def get_groups(accounts):
|
||||
for account in accounts:
|
||||
if account["NAME"] in (self.tally_creditors_account, self.tally_debtors_account):
|
||||
yield get_parent(account), account["NAME"], 0
|
||||
else:
|
||||
yield get_parent(account), account["NAME"], 1
|
||||
|
||||
def get_ledgers(accounts):
|
||||
for account in accounts:
|
||||
# If Ledger doesn't have PARENT field then don't create Account
|
||||
# For example "Profit & Loss A/c"
|
||||
if account.PARENT:
|
||||
yield account.PARENT.string.strip(), account["NAME"], 0
|
||||
|
||||
def get_parent(account):
|
||||
if account.PARENT:
|
||||
return account.PARENT.string.strip()
|
||||
return {
|
||||
("Yes", "No"): "Application of Funds (Assets)",
|
||||
("Yes", "Yes"): "Expenses",
|
||||
("No", "Yes"): "Income",
|
||||
("No", "No"): "Source of Funds (Liabilities)",
|
||||
}[(account.ISDEEMEDPOSITIVE.string.strip(), account.ISREVENUE.string.strip())]
|
||||
|
||||
def get_children_and_parent_dict(accounts):
|
||||
children, parents = {}, {}
|
||||
for parent, account, _is_group in accounts:
|
||||
children.setdefault(parent, set()).add(account)
|
||||
parents.setdefault(account, set()).add(parent)
|
||||
parents[account].update(parents.get(parent, []))
|
||||
return children, parents
|
||||
|
||||
def remove_parties(parents, children, group_set):
|
||||
customers, suppliers = set(), set()
|
||||
for account in parents:
|
||||
found = False
|
||||
if self.tally_creditors_account in parents[account]:
|
||||
found = True
|
||||
if account not in group_set:
|
||||
suppliers.add(account)
|
||||
if self.tally_debtors_account in parents[account]:
|
||||
found = True
|
||||
if account not in group_set:
|
||||
customers.add(account)
|
||||
if found:
|
||||
children.pop(account, None)
|
||||
|
||||
return children, customers, suppliers
|
||||
|
||||
def traverse(tree, children, accounts, roots, group_set):
|
||||
for account in accounts:
|
||||
if account in group_set or account in roots:
|
||||
if account in children:
|
||||
tree[account] = traverse({}, children, children[account], roots, group_set)
|
||||
else:
|
||||
tree[account] = {"is_group": 1}
|
||||
else:
|
||||
tree[account] = {}
|
||||
return tree
|
||||
|
||||
def get_parties_addresses(collection, customers, suppliers):
|
||||
parties, addresses = [], []
|
||||
for account in collection.find_all("LEDGER"):
|
||||
party_type = None
|
||||
links = []
|
||||
if account.NAME.string.strip() in customers:
|
||||
party_type = "Customer"
|
||||
parties.append(
|
||||
{
|
||||
"doctype": party_type,
|
||||
"customer_name": account.NAME.string.strip(),
|
||||
"tax_id": account.INCOMETAXNUMBER.string.strip()
|
||||
if account.INCOMETAXNUMBER
|
||||
else None,
|
||||
"customer_group": "All Customer Groups",
|
||||
"territory": "All Territories",
|
||||
"customer_type": "Individual",
|
||||
}
|
||||
)
|
||||
links.append({"link_doctype": party_type, "link_name": account["NAME"]})
|
||||
|
||||
if account.NAME.string.strip() in suppliers:
|
||||
party_type = "Supplier"
|
||||
parties.append(
|
||||
{
|
||||
"doctype": party_type,
|
||||
"supplier_name": account.NAME.string.strip(),
|
||||
"pan": account.INCOMETAXNUMBER.string.strip()
|
||||
if account.INCOMETAXNUMBER
|
||||
else None,
|
||||
"supplier_group": "All Supplier Groups",
|
||||
"supplier_type": "Individual",
|
||||
}
|
||||
)
|
||||
links.append({"link_doctype": party_type, "link_name": account["NAME"]})
|
||||
|
||||
if party_type:
|
||||
address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")])
|
||||
addresses.append(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_line1": address[:140].strip(),
|
||||
"address_line2": address[140:].strip(),
|
||||
"country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None,
|
||||
"state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
|
||||
"gst_state": account.LEDSTATENAME.string.strip()
|
||||
if account.LEDSTATENAME
|
||||
else None,
|
||||
"pin_code": account.PINCODE.string.strip() if account.PINCODE else None,
|
||||
"mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
|
||||
"phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
|
||||
"gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None,
|
||||
"links": links,
|
||||
}
|
||||
)
|
||||
return parties, addresses
|
||||
|
||||
def get_stock_items_uoms(collection):
|
||||
uoms = []
|
||||
for uom in collection.find_all("UNIT"):
|
||||
uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string.strip()})
|
||||
|
||||
items = []
|
||||
for item in collection.find_all("STOCKITEM"):
|
||||
stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom
|
||||
items.append(
|
||||
{
|
||||
"doctype": "Item",
|
||||
"item_code": item.NAME.string.strip(),
|
||||
"stock_uom": stock_uom.strip(),
|
||||
"is_stock_item": 0,
|
||||
"item_group": "All Item Groups",
|
||||
"item_defaults": [{"company": self.erpnext_company}],
|
||||
}
|
||||
)
|
||||
|
||||
return items, uoms
|
||||
|
||||
try:
|
||||
self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
|
||||
collection = self.get_collection(self.master_data)
|
||||
company = get_company_name(collection)
|
||||
self.tally_company = company
|
||||
self.erpnext_company = company
|
||||
|
||||
self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
|
||||
chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
|
||||
|
||||
self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
|
||||
parties, addresses = get_parties_addresses(collection, customers, suppliers)
|
||||
|
||||
self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
|
||||
items, uoms = get_stock_items_uoms(collection)
|
||||
data = {
|
||||
"chart_of_accounts": chart_of_accounts,
|
||||
"parties": parties,
|
||||
"addresses": addresses,
|
||||
"items": items,
|
||||
"uoms": uoms,
|
||||
}
|
||||
|
||||
self.publish("Process Master Data", _("Done"), 5, 5)
|
||||
self.dump_processed_data(data)
|
||||
|
||||
self.is_master_data_processed = 1
|
||||
|
||||
except Exception:
|
||||
self.publish("Process Master Data", _("Process Failed"), -1, 5)
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def publish(self, title, message, count, total):
|
||||
frappe.publish_realtime(
|
||||
"tally_migration_progress_update",
|
||||
{"title": title, "message": message, "count": count, "total": total},
|
||||
user=self.modified_by,
|
||||
)
|
||||
|
||||
def _import_master_data(self):
|
||||
def create_company_and_coa(coa_file_url):
|
||||
coa_file = frappe.get_doc("File", {"file_url": coa_file_url})
|
||||
frappe.local.flags.ignore_chart_of_accounts = True
|
||||
|
||||
try:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": self.erpnext_company,
|
||||
"default_currency": "INR",
|
||||
"enable_perpetual_inventory": 0,
|
||||
}
|
||||
).insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
company = frappe.get_doc("Company", self.erpnext_company)
|
||||
unset_existing_data(self.erpnext_company)
|
||||
|
||||
frappe.local.flags.ignore_chart_of_accounts = False
|
||||
create_charts(company.name, custom_chart=json.loads(coa_file.get_content()))
|
||||
company.create_default_warehouses()
|
||||
|
||||
def create_parties_and_addresses(parties_file_url, addresses_file_url):
|
||||
parties_file = frappe.get_doc("File", {"file_url": parties_file_url})
|
||||
for party in json.loads(parties_file.get_content()):
|
||||
try:
|
||||
party_doc = frappe.get_doc(party)
|
||||
party_doc.insert()
|
||||
except Exception:
|
||||
self.log(party_doc)
|
||||
addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url})
|
||||
for address in json.loads(addresses_file.get_content()):
|
||||
try:
|
||||
address_doc = frappe.get_doc(address)
|
||||
address_doc.insert(ignore_mandatory=True)
|
||||
except Exception:
|
||||
self.log(address_doc)
|
||||
|
||||
def create_items_uoms(items_file_url, uoms_file_url):
|
||||
uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url})
|
||||
for uom in json.loads(uoms_file.get_content()):
|
||||
if not frappe.db.exists(uom):
|
||||
try:
|
||||
uom_doc = frappe.get_doc(uom)
|
||||
uom_doc.insert()
|
||||
except Exception:
|
||||
self.log(uom_doc)
|
||||
|
||||
items_file = frappe.get_doc("File", {"file_url": items_file_url})
|
||||
for item in json.loads(items_file.get_content()):
|
||||
try:
|
||||
item_doc = frappe.get_doc(item)
|
||||
item_doc.insert()
|
||||
except Exception:
|
||||
self.log(item_doc)
|
||||
|
||||
try:
|
||||
self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
|
||||
create_company_and_coa(self.chart_of_accounts)
|
||||
|
||||
self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
|
||||
create_parties_and_addresses(self.parties, self.addresses)
|
||||
|
||||
self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
|
||||
create_items_uoms(self.items, self.uoms)
|
||||
|
||||
self.publish("Import Master Data", _("Done"), 4, 4)
|
||||
|
||||
self.set_account_defaults()
|
||||
self.is_master_data_imported = 1
|
||||
frappe.db.commit()
|
||||
|
||||
except Exception:
|
||||
self.publish("Import Master Data", _("Process Failed"), -1, 5)
|
||||
frappe.db.rollback()
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _process_day_book_data(self):
|
||||
def get_vouchers(collection):
|
||||
vouchers = []
|
||||
for voucher in collection.find_all("VOUCHER"):
|
||||
if voucher.ISCANCELLED.string.strip() == "Yes":
|
||||
continue
|
||||
inventory_entries = (
|
||||
voucher.find_all("INVENTORYENTRIES.LIST")
|
||||
+ voucher.find_all("ALLINVENTORYENTRIES.LIST")
|
||||
+ voucher.find_all("INVENTORYENTRIESIN.LIST")
|
||||
+ voucher.find_all("INVENTORYENTRIESOUT.LIST")
|
||||
)
|
||||
if (
|
||||
voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"]
|
||||
and inventory_entries
|
||||
):
|
||||
function = voucher_to_invoice
|
||||
else:
|
||||
function = voucher_to_journal_entry
|
||||
try:
|
||||
processed_voucher = function(voucher)
|
||||
if processed_voucher:
|
||||
vouchers.append(processed_voucher)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
self.log(voucher)
|
||||
return vouchers
|
||||
|
||||
def voucher_to_journal_entry(voucher):
|
||||
accounts = []
|
||||
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all(
|
||||
"LEDGERENTRIES.LIST"
|
||||
)
|
||||
for entry in ledger_entries:
|
||||
account = {
|
||||
"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company),
|
||||
"cost_center": self.default_cost_center,
|
||||
}
|
||||
if entry.ISPARTYLEDGER.string.strip() == "Yes":
|
||||
party_details = get_party(entry.LEDGERNAME.string.strip())
|
||||
if party_details:
|
||||
party_type, party_account = party_details
|
||||
account["party_type"] = party_type
|
||||
account["account"] = party_account
|
||||
account["party"] = entry.LEDGERNAME.string.strip()
|
||||
amount = Decimal(entry.AMOUNT.string.strip())
|
||||
if amount > 0:
|
||||
account["credit_in_account_currency"] = str(abs(amount))
|
||||
else:
|
||||
account["debit_in_account_currency"] = str(abs(amount))
|
||||
accounts.append(account)
|
||||
|
||||
journal_entry = {
|
||||
"doctype": "Journal Entry",
|
||||
"tally_guid": voucher.GUID.string.strip(),
|
||||
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
|
||||
"posting_date": voucher.DATE.string.strip(),
|
||||
"company": self.erpnext_company,
|
||||
"accounts": accounts,
|
||||
}
|
||||
return journal_entry
|
||||
|
||||
def voucher_to_invoice(voucher):
|
||||
if voucher.VOUCHERTYPENAME.string.strip() in ["Sales", "Credit Note"]:
|
||||
doctype = "Sales Invoice"
|
||||
party_field = "customer"
|
||||
account_field = "debit_to"
|
||||
account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
|
||||
price_list_field = "selling_price_list"
|
||||
elif voucher.VOUCHERTYPENAME.string.strip() in ["Purchase", "Debit Note"]:
|
||||
doctype = "Purchase Invoice"
|
||||
party_field = "supplier"
|
||||
account_field = "credit_to"
|
||||
account_name = encode_company_abbr(self.tally_creditors_account, self.erpnext_company)
|
||||
price_list_field = "buying_price_list"
|
||||
else:
|
||||
# Do not handle vouchers other than "Purchase", "Debit Note", "Sales" and "Credit Note"
|
||||
# Do not handle Custom Vouchers either
|
||||
return
|
||||
|
||||
invoice = {
|
||||
"doctype": doctype,
|
||||
party_field: voucher.PARTYNAME.string.strip(),
|
||||
"tally_guid": voucher.GUID.string.strip(),
|
||||
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
|
||||
"posting_date": voucher.DATE.string.strip(),
|
||||
"due_date": voucher.DATE.string.strip(),
|
||||
"items": get_voucher_items(voucher, doctype),
|
||||
"taxes": get_voucher_taxes(voucher),
|
||||
account_field: account_name,
|
||||
price_list_field: "Tally Price List",
|
||||
"set_posting_time": 1,
|
||||
"disable_rounded_total": 1,
|
||||
"company": self.erpnext_company,
|
||||
}
|
||||
return invoice
|
||||
|
||||
def get_voucher_items(voucher, doctype):
|
||||
inventory_entries = (
|
||||
voucher.find_all("INVENTORYENTRIES.LIST")
|
||||
+ voucher.find_all("ALLINVENTORYENTRIES.LIST")
|
||||
+ voucher.find_all("INVENTORYENTRIESIN.LIST")
|
||||
+ voucher.find_all("INVENTORYENTRIESOUT.LIST")
|
||||
)
|
||||
if doctype == "Sales Invoice":
|
||||
account_field = "income_account"
|
||||
elif doctype == "Purchase Invoice":
|
||||
account_field = "expense_account"
|
||||
items = []
|
||||
for entry in inventory_entries:
|
||||
qty, uom = entry.ACTUALQTY.string.strip().split()
|
||||
items.append(
|
||||
{
|
||||
"item_code": entry.STOCKITEMNAME.string.strip(),
|
||||
"description": entry.STOCKITEMNAME.string.strip(),
|
||||
"qty": qty.strip(),
|
||||
"uom": uom.strip(),
|
||||
"conversion_factor": 1,
|
||||
"price_list_rate": entry.RATE.string.strip().split("/")[0],
|
||||
"cost_center": self.default_cost_center,
|
||||
"warehouse": self.default_warehouse,
|
||||
account_field: encode_company_abbr(
|
||||
entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(),
|
||||
self.erpnext_company,
|
||||
),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
def get_voucher_taxes(voucher):
|
||||
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all(
|
||||
"LEDGERENTRIES.LIST"
|
||||
)
|
||||
taxes = []
|
||||
for entry in ledger_entries:
|
||||
if entry.ISPARTYLEDGER.string.strip() == "No":
|
||||
tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company)
|
||||
taxes.append(
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": tax_account,
|
||||
"description": tax_account,
|
||||
"tax_amount": entry.AMOUNT.string.strip(),
|
||||
"cost_center": self.default_cost_center,
|
||||
}
|
||||
)
|
||||
return taxes
|
||||
|
||||
def get_party(party):
|
||||
if frappe.db.exists({"doctype": "Supplier", "supplier_name": party}):
|
||||
return "Supplier", encode_company_abbr(self.tally_creditors_account, self.erpnext_company)
|
||||
elif frappe.db.exists({"doctype": "Customer", "customer_name": party}):
|
||||
return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
|
||||
|
||||
try:
|
||||
self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
|
||||
collection = self.get_collection(self.day_book_data)
|
||||
|
||||
self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
|
||||
vouchers = get_vouchers(collection)
|
||||
|
||||
self.publish("Process Day Book Data", _("Done"), 3, 3)
|
||||
self.dump_processed_data({"vouchers": vouchers})
|
||||
|
||||
self.is_day_book_data_processed = 1
|
||||
|
||||
except Exception:
|
||||
self.publish("Process Day Book Data", _("Process Failed"), -1, 5)
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _import_day_book_data(self):
|
||||
def create_fiscal_years(vouchers):
|
||||
from frappe.utils.data import add_years, getdate
|
||||
|
||||
earliest_date = getdate(min(voucher["posting_date"] for voucher in vouchers))
|
||||
oldest_year = frappe.get_all(
|
||||
"Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date"
|
||||
)[0]
|
||||
while earliest_date < oldest_year.year_start_date:
|
||||
new_year = frappe.get_doc({"doctype": "Fiscal Year"})
|
||||
new_year.year_start_date = add_years(oldest_year.year_start_date, -1)
|
||||
new_year.year_end_date = add_years(oldest_year.year_end_date, -1)
|
||||
if new_year.year_start_date.year == new_year.year_end_date.year:
|
||||
new_year.year = new_year.year_start_date.year
|
||||
else:
|
||||
new_year.year = f"{new_year.year_start_date.year}-{new_year.year_end_date.year}"
|
||||
new_year.save()
|
||||
oldest_year = new_year
|
||||
|
||||
def create_custom_fields():
|
||||
_create_custom_fields(
|
||||
{
|
||||
("Journal Entry", "Purchase Invoice", "Sales Invoice"): [
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "tally_guid",
|
||||
"read_only": 1,
|
||||
"label": "Tally GUID",
|
||||
},
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "tally_voucher_no",
|
||||
"read_only": 1,
|
||||
"label": "Tally Voucher Number",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
def create_price_list():
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Price List",
|
||||
"price_list_name": "Tally Price List",
|
||||
"selling": 1,
|
||||
"buying": 1,
|
||||
"enabled": 1,
|
||||
"currency": "INR",
|
||||
}
|
||||
).insert()
|
||||
|
||||
try:
|
||||
frappe.db.set_value(
|
||||
"Account",
|
||||
encode_company_abbr(self.tally_creditors_account, self.erpnext_company),
|
||||
"account_type",
|
||||
"Payable",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Account",
|
||||
encode_company_abbr(self.tally_debtors_account, self.erpnext_company),
|
||||
"account_type",
|
||||
"Receivable",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company", self.erpnext_company, "round_off_account", self.default_round_off_account
|
||||
)
|
||||
|
||||
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
|
||||
vouchers = json.loads(vouchers_file.get_content())
|
||||
|
||||
create_fiscal_years(vouchers)
|
||||
create_price_list()
|
||||
create_custom_fields()
|
||||
|
||||
total = len(vouchers)
|
||||
is_last = False
|
||||
|
||||
for index in range(0, total, VOUCHER_CHUNK_SIZE):
|
||||
if index + VOUCHER_CHUNK_SIZE >= total:
|
||||
is_last = True
|
||||
frappe.enqueue_doc(
|
||||
self.doctype,
|
||||
self.name,
|
||||
"_import_vouchers",
|
||||
queue="long",
|
||||
timeout=3600,
|
||||
start=index + 1,
|
||||
total=total,
|
||||
is_last=is_last,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _import_vouchers(self, start, total, is_last=False):
|
||||
frappe.flags.in_migrate = True
|
||||
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
|
||||
vouchers = json.loads(vouchers_file.get_content())
|
||||
chunk = vouchers[start : start + VOUCHER_CHUNK_SIZE]
|
||||
|
||||
for index, voucher in enumerate(chunk, start=start):
|
||||
try:
|
||||
voucher_doc = frappe.get_doc(voucher)
|
||||
voucher_doc.insert()
|
||||
voucher_doc.submit()
|
||||
self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
self.log(voucher_doc)
|
||||
|
||||
if is_last:
|
||||
self.status = ""
|
||||
self.is_day_book_data_imported = 1
|
||||
self.save()
|
||||
frappe.db.set_value("Price List", "Tally Price List", "enabled", 0)
|
||||
frappe.flags.in_migrate = False
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_master_data(self):
|
||||
self.set_status("Processing Master Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_master_data(self):
|
||||
self.set_status("Importing Master Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_day_book_data(self):
|
||||
self.set_status("Processing Day Book Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_day_book_data(self):
|
||||
self.set_status("Importing Day Book Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
|
||||
|
||||
def log(self, data=None):
|
||||
if isinstance(data, frappe.model.document.Document):
|
||||
if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError:
|
||||
failed_import_log = json.loads(self.failed_import_log)
|
||||
doc = data.as_dict()
|
||||
failed_import_log.append({"doc": doc, "exc": traceback.format_exc()})
|
||||
self.failed_import_log = json.dumps(failed_import_log, separators=(",", ":"))
|
||||
self.save()
|
||||
frappe.db.commit()
|
||||
|
||||
else:
|
||||
data = data or self.status
|
||||
message = "\n".join(
|
||||
[
|
||||
"Data:",
|
||||
json.dumps(data, default=str, indent=4),
|
||||
"--" * 50,
|
||||
"\nException:",
|
||||
traceback.format_exc(),
|
||||
]
|
||||
)
|
||||
return frappe.log_error(title="Tally Migration Error", message=message)
|
||||
|
||||
def set_status(self, status=""):
|
||||
self.status = status
|
||||
self.save()
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestTallyMigration(IntegrationTestCase):
|
||||
pass
|
||||
Reference in New Issue
Block a user