chore: removal of tally migration feature (#45100)

This commit is contained in:
Diptanil Saha
2025-01-07 17:28:46 +05:30
committed by GitHub
parent a0f17f8e73
commit 9f77793f16
5 changed files with 0 additions and 1421 deletions

View File

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

View File

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

View File

@@ -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("&#4;", "", 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()

View File

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