feat: Tally Migration (#17405)
feat: Tally Migration
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/__init__.py b/erpnext/erpnext_integrations/doctype/tally_migration/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
new file mode 100644
index 0000000..104ac57
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Tally Migration', {
+ onload: function(frm) {
+ frappe.realtime.on("tally_migration_progress_update", function (data) {
+ frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
+ if (data.count == data.total) {
+ window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title);
+ }
+ });
+ },
+ refresh: function(frm) {
+ 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");
+ }
+ }
+ }
+ },
+ add_button: function(frm, label, method) {
+ frm.add_custom_button(
+ label,
+ () => frm.call({
+ doc: frm.doc,
+ method: method,
+ freeze: true,
+ callback: () => {
+ frm.remove_custom_button(label);
+ }
+ })
+ );
+ }
+});
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
new file mode 100644
index 0000000..26415ca
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
@@ -0,0 +1,219 @@
+{
+ "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",
+ "column_break_8",
+ "erpnext_company",
+ "processed_files_section",
+ "chart_of_accounts",
+ "parties",
+ "addresses",
+ "column_break_17",
+ "uoms",
+ "items",
+ "vouchers",
+ "accounts_section",
+ "default_warehouse",
+ "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"
+ ],
+ "fields": [
+ {
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Status"
+ },
+ {
+ "fieldname": "master_data",
+ "fieldtype": "Attach",
+ "in_list_view": 1,
+ "label": "Master Data"
+ },
+ {
+ "default": "Sundry Creditors",
+ "fieldname": "tally_creditors_account",
+ "fieldtype": "Data",
+ "label": "Tally Creditors Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Sundry Debtors",
+ "fieldname": "tally_debtors_account",
+ "fieldtype": "Data",
+ "label": "Tally Debtors Account",
+ "reqd": 1
+ },
+ {
+ "depends_on": "is_master_data_processed",
+ "fieldname": "company_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "tally_company",
+ "fieldtype": "Data",
+ "label": "Tally Company",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "erpnext_company",
+ "fieldtype": "Data",
+ "label": "ERPNext Company"
+ },
+ {
+ "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",
+ "fieldname": "accounts_section",
+ "fieldtype": "Section Break",
+ "label": "Accounts"
+ },
+ {
+ "fieldname": "default_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "round_off_account",
+ "fieldtype": "Link",
+ "label": "Round Off Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_cost_center",
+ "fieldtype": "Link",
+ "label": "Default Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "is_master_data_processed",
+ "fieldtype": "Check",
+ "label": "Is Master Data Processed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "is_day_book_data_processed",
+ "fieldtype": "Check",
+ "label": "Is Day Book Data Processed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "is_day_book_data_imported",
+ "fieldtype": "Check",
+ "label": "Is Day Book Data Imported",
+ "read_only": 1
+ },
+ {
+ "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"
+ },
+ {
+ "fieldname": "day_book_data",
+ "fieldtype": "Attach",
+ "in_list_view": 1,
+ "label": "Day Book Data"
+ }
+ ],
+ "modified": "2019-04-29 05:46:54.394967",
+ "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": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
new file mode 100644
index 0000000..12b646d
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -0,0 +1,512 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+from decimal import Decimal
+import json
+import re
+import traceback
+import zipfile
+import frappe
+from frappe import _
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
+from frappe.model.document import Document
+from frappe.model.naming import getseries, revert_series_if_last
+from frappe.utils.data import format_datetime
+from bs4 import BeautifulSoup as bs
+from erpnext import encode_company_abbr
+from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
+
+PRIMARY_ACCOUNT = "Primary"
+VOUCHER_CHUNK_SIZE = 500
+
+
+class TallyMigration(Document):
+ 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})
+
+ with zipfile.ZipFile(master_file.get_full_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)
+ }).insert()
+ setattr(self, key, f.file_url)
+
+ def _process_master_data(self):
+ def get_company_name(collection):
+ return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string
+
+ 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)
+ coa = traverse({}, children, roots, roots, group_set)
+
+ 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, account["NAME"], 0
+
+ def get_parent(account):
+ if account.PARENT:
+ return account.PARENT.string
+ return {
+ ("Yes", "No"): "Application of Funds (Assets)",
+ ("Yes", "Yes"): "Expenses",
+ ("No", "Yes"): "Income",
+ ("No", "No"): "Source of Funds (Liabilities)",
+ }[(account.ISDEEMEDPOSITIVE.string, account.ISREVENUE.string)]
+
+ 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:
+ if self.tally_creditors_account in parents[account]:
+ children.pop(account, None)
+ if account not in group_set:
+ suppliers.add(account)
+ elif self.tally_debtors_account in parents[account]:
+ children.pop(account, None)
+ if account not in group_set:
+ customers.add(account)
+ 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
+ if account.NAME.string in customers:
+ party_type = "Customer"
+ parties.append({
+ "doctype": party_type,
+ "customer_name": account.NAME.string,
+ "tax_id": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
+ "customer_group": "All Customer Groups",
+ "territory": "All Territories",
+ "customer_type": "Individual",
+ })
+ elif account.NAME.string in suppliers:
+ party_type = "Supplier"
+ parties.append({
+ "doctype": party_type,
+ "supplier_name": account.NAME.string,
+ "pan": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Individual",
+ })
+ if party_type:
+ address = "\n".join([a.string 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 if account.COUNTRYNAME else None,
+ "state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
+ "gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
+ "pin_code": account.PINCODE.string if account.PINCODE else None,
+ "mobile": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
+ "phone": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
+ "gstin": account.PARTYGSTIN.string if account.PARTYGSTIN else None,
+ "links": [{"link_doctype": party_type, "link_name": account["NAME"]}],
+ })
+ 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})
+
+ items = []
+ for item in collection.find_all("STOCKITEM"):
+ items.append({
+ "doctype": "Item",
+ "item_code" : item.NAME.string,
+ "stock_uom": item.BASEUNITS.string,
+ "is_stock_item": 0,
+ "item_group": "All Item Groups",
+ "item_defaults": [{"company": self.erpnext_company}]
+ })
+ return items, uoms
+
+
+ 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
+ self.status = ""
+ self.save()
+
+ def publish(self, title, message, count, total):
+ frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total})
+
+ 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
+ company = frappe.get_doc({
+ "doctype": "Company",
+ "company_name": self.erpnext_company,
+ "default_currency": "INR",
+ "enable_perpetual_inventory": 0,
+ }).insert()
+ 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:
+ frappe.get_doc(party).insert()
+ except:
+ self.log(party)
+ addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url})
+ for address in json.loads(addresses_file.get_content()):
+ try:
+ frappe.get_doc(address).insert(ignore_mandatory=True)
+ except:
+ try:
+ gstin = address.pop("gstin", None)
+ frappe.get_doc(address).insert(ignore_mandatory=True)
+ self.log({"address": address, "message": "Invalid GSTIN: {}. Address was created without GSTIN".format(gstin)})
+ except:
+ self.log(address)
+
+
+ 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:
+ frappe.get_doc(uom).insert()
+ except:
+ self.log(uom)
+
+ items_file = frappe.get_doc("File", {"file_url": items_file_url})
+ for item in json.loads(items_file.get_content()):
+ try:
+ frappe.get_doc(item).insert()
+ except:
+ self.log(item)
+
+ 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.status = ""
+ self.is_master_data_imported = 1
+ self.save()
+
+ def _process_day_book_data(self):
+ def get_vouchers(collection):
+ vouchers = []
+ for voucher in collection.find_all("VOUCHER"):
+ if voucher.ISCANCELLED.string == "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 not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries:
+ function = voucher_to_invoice
+ else:
+ function = voucher_to_journal_entry
+ try:
+ vouchers.append(function(voucher))
+ except:
+ 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, self.erpnext_company), "cost_center": self.default_cost_center}
+ if entry.ISPARTYLEDGER.string == "Yes":
+ party_details = get_party(entry.LEDGERNAME.string)
+ if party_details:
+ party_type, party_account = party_details
+ account["party_type"] = party_type
+ account["account"] = party_account
+ account["party"] = entry.LEDGERNAME.string
+ amount = Decimal(entry.AMOUNT.string)
+ 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,
+ "posting_date": voucher.DATE.string,
+ "company": self.erpnext_company,
+ "accounts": accounts,
+ }
+ return journal_entry
+
+ def voucher_to_invoice(voucher):
+ if voucher.VOUCHERTYPENAME.string 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 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"
+
+ invoice = {
+ "doctype": doctype,
+ party_field: voucher.PARTYNAME.string,
+ "tally_guid": voucher.GUID.string,
+ "posting_date": voucher.DATE.string,
+ "due_date": voucher.DATE.string,
+ "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,
+ "description": entry.STOCKITEMNAME.string,
+ "qty": qty.strip(),
+ "uom": uom.strip(),
+ "conversion_factor": 1,
+ "price_list_rate": entry.RATE.string.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, 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 == "No":
+ tax_account = encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company)
+ taxes.append({
+ "charge_type": "Actual",
+ "account_head": tax_account,
+ "description": tax_account,
+ "tax_amount": entry.AMOUNT.string,
+ "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)
+
+ 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.status = ""
+ self.is_day_book_data_processed = 1
+ self.save()
+
+ 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 = "{}-{}".format(new_year.year_start_date.year, new_year.year_end_date.year)
+ new_year.save()
+ oldest_year = new_year
+
+ def create_custom_fields(doctypes):
+ for doctype in doctypes:
+ df = {
+ "fieldtype": "Data",
+ "fieldname": "tally_guid",
+ "read_only": 1,
+ "label": "Tally GUID"
+ }
+ create_custom_field(doctype, df)
+
+ 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()
+
+ 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.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(["Journal Entry", "Purchase Invoice", "Sales Invoice"])
+
+ 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)
+
+ 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:
+ doc = frappe.get_doc(voucher).insert()
+ doc.submit()
+ self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total)
+ except:
+ self.log(voucher)
+
+ 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
+
+ def process_master_data(self):
+ self.status = "Processing Master Data"
+ self.save()
+ frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
+
+ def import_master_data(self):
+ self.status = "Importing Master Data"
+ self.save()
+ frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
+
+ def process_day_book_data(self):
+ self.status = "Processing Day Book Data"
+ self.save()
+ frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
+
+ def import_day_book_data(self):
+ self.status = "Importing Day Book Data"
+ self.save()
+ frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
+
+ def log(self, data=None):
+ message = "\n".join(["Data", json.dumps(data, default=str, indent=4), "Exception", traceback.format_exc()])
+ return frappe.log_error(title="Tally Migration Error", message=message)
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js
new file mode 100644
index 0000000..433c5e2
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Tally Migration", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Tally Migration
+ () => frappe.tests.make('Tally Migration', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.py
new file mode 100644
index 0000000..9f67e55
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestTallyMigration(unittest.TestCase):
+ pass