feat(tally): Preprocess and create parties and addresses
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
index 2087c7f..6478cdb 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
@@ -368,6 +368,70 @@
    "set_only_once": 0,
    "translatable": 0,
    "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "parties",
+   "fieldtype": "Attach",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Parties",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "addresses",
+   "fieldtype": "Attach",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Addresses",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
   }
  ],
  "has_web_view": 0,
@@ -380,7 +444,7 @@
  "issingle": 0,
  "istable": 0,
  "max_attachments": 0,
- "modified": "2019-03-01 20:58:04.320605",
+ "modified": "2019-03-01 22:44:04.042954",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "Tally Migration",
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index aac371d..c8e2eba 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -6,6 +6,7 @@
 
 import json
 import re
+import traceback
 import zipfile
 import frappe
 from frappe.model.document import Document
@@ -17,6 +18,7 @@
 class TallyMigration(Document):
 	def _preprocess(self):
 		company, chart_of_accounts_tree, customers, suppliers = self._process_master_data()
+		parties, addresses = self._process_parties(customers, suppliers)
 		self.tally_company = company
 		self.erpnext_company = company
 		self.status = "Preprocessed"
@@ -29,6 +31,25 @@
 			"content": json.dumps(chart_of_accounts_tree)
 		}).insert()
 		self.chart_of_accounts = coa_file.file_url
+
+		parties_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "Parties.json",
+			"attached_to_doctype": self.doctype,
+			"attached_to_name": self.name,
+			"content": json.dumps(parties)
+		}).insert()
+		self.parties = parties_file.file_url
+
+		addresses_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "Addresses.json",
+			"attached_to_doctype": self.doctype,
+			"attached_to_name": self.name,
+			"content": json.dumps(addresses)
+		}).insert()
+		self.addresses = addresses_file.file_url
+
 		self.save()
 
 	def _process_master_data(self):
@@ -93,6 +114,7 @@
 			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):
@@ -105,7 +127,7 @@
 				elif self.tally_debtors_account in parents[account]:
 					children.pop(account, None)
 					if account not in group_set:
-							suppliers.add(account)
+						suppliers.add(account)
 			return children, customers, suppliers
 
 		def traverse(tree, children, accounts, roots, group_set):
@@ -126,10 +148,63 @@
 
 		return company, chart_of_accounts_tree, customer_names, supplier_names
 
+	def _process_parties(self, customers, suppliers):
+		def get_master_collection(master_data):
+			master_file = frappe.get_doc("File", {"file_url": master_data})
+
+			with zipfile.ZipFile(master_file.get_full_path()) as zf:
+				content = zf.read(zf.namelist()[0]).decode("utf-16")
+
+			master = bs(sanitize(emptify(content)), "xml")
+			collection = master.BODY.IMPORTDATA.REQUESTDATA
+			return collection
+
+		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")[:2]])
+					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.STATENAME.string if account.STATENAME else None,
+						"gst_state": account.STATENAME.string if account.STATENAME else None,
+						"pin_code": account.PINCODE.string if account.PINCODE else None,
+						"gstin": account.PARTYGSTIN.string if account.PARTYGSTIN else None,
+						"links": [{"link_doctype": party_type, "link_name": account["NAME"]}],
+					})
+			return parties, addresses
+
+		collection = get_master_collection(self.master_data)
+		parties, addresses = get_parties_addresses(collection, customers, suppliers)
+		return parties, addresses
+
 	def preprocess(self):
 		frappe.enqueue_doc(self.doctype, self.name, "_preprocess")
 
-	def start_import(self):
+	def _start_import(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
@@ -141,8 +216,30 @@
 			frappe.local.flags.ignore_chart_of_accounts = False
 			create_charts(company.name, json.loads(coa_file.get_content()))
 
-		create_company_and_coa(self.chart_of_accounts)
+		def create_parties_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:
+					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:
+					log(address)
 
+		create_company_and_coa(self.chart_of_accounts)
+		create_parties_addresses(self.parties, self.addresses)
+
+	def start_import(self):
+		frappe.enqueue_doc(self.doctype, self.name, "_start_import")
+
+
+def log(data=None):
+	message = json.dumps({"data": data, "exception": traceback.format_exc()}, indent=4)
+	frappe.log_error(title="Tally Migration Error", message=message)
 
 def sanitize(string):
 	return re.sub("", "", string)