using System; using System.Windows.Forms; using GZTW.AyaNova.BLL; using System.Data; using System.Drawing; using System.Reflection; using System.Collections; using System.Text; using System.Threading; using System.Collections.Generic; using System.Text.RegularExpressions; //auth using DesktopIppOAuth; //qbo using Intuit.Ipp.Core; using q = Intuit.Ipp.Data; using Intuit.Ipp.QueryFilter; using Intuit.Ipp.DataService; using Intuit.Ipp.Exception; //extra using System.Linq; //QUICKBOOKS ONLINE INTERFACE //MAIN CASE WITH NOTES: //https://fog.ayanova.com/default.asp?3217 /* * Developer account: * Go to https://developer.intuit.com/ Sign in using: email: support@ayanova.com pw: i8BREAKfast! * * Current test account * Expires 7/26/2017 * gzmailadmin@gmail.com Abraxis2017# * * "MyTestCo" supports inventory * OfferingSku="QuickBooks Online Plus" * US expires 8/26/2017 * cardjohn@ayanova.com * Abraxis2017# * 7/27 "MyTestSimpleStart" simple start does not support inventory * OfferingSku="QuickBooks Online Simple Start" * * Sales@ayanova.com * Abraxis2017# * * 7/27 * DOESN"T WORK WITH QBOI as it isn't an accounting program * Contractor self employed edition * * webmaster@ayanova.com * Abraxis2017# * * 7/27 Essentials * OfferingSku="QuickBooks Online Essentials" * addressinator@ayanova.com * Abraxis2017# * * * support@ayanova.com * AyaNovaTestCompany * OfferingSku="QuickBooks Online EasyStart" * * QBOI_TEST_INVENTORY_COMPANY * OfferingSku="QuickBooks Online EasyStart" * * Dec 4 2017 "Plus" * webmaster@ayanova.com * Abraxis2017# * */ namespace AyaNova.PlugIn.QBOI { /// /// Summary description for Util. /// public class Util { public enum pfstat { OK = 0, Failed = 1, Cancel = 2 } public static void SetControlBackQBOIColor(Control f) { f.BackColor = System.Drawing.Color.PaleGreen; } #region Attributes and properties public static string testAuthToken = string.Empty; public static Global GlobalSettings = null; // public static GZTW.AyaNova.BLL.Region RegionalSettings = null; public static LocalizedTextTable LocaleText = null; public static Integration QBOIntegration = null; public static QBIDataEx QDat = null; public static string QCountry = "US"; public static bool QUSA = true; public static double QVersion = 1.1; public static string QCompanyFile = ""; public static string QCompanyName = ""; public static string sLastRequestXML = ""; public static System.Resources.ResourceManager AyaResource = null; //case 3296 public static Dictionary QCompOtherPrefs = new Dictionary(); //whether the QB company supports inventory or not //In testing AU, CA, UK and US all support inventory if OfferingSku is "Quickbooks online plus" and not if any other value // public static bool QBSupportsInventory = false; public static string QOfferingSku = ""; //case 1062 public static bool bMainWindowOpen = false; /// /// The official and unchanging integration ID for /// QBOI /// public static Guid QBID { get { return new Guid("{DE6694A6-7F78-44A1-A732-83DAF0407AE9}"); } } public static Guid _ImportPartToAyaNovaDefaultVendorId = Guid.Empty; public static string _ImportPartToQBDefaultTaxCodeId = string.Empty; public static string _ImportRateToQBDefaultTaxCodeId = string.Empty; public static bool UseInventory//TODO CASE 3296 { get { return AyaBizUtils.GlobalSettings.UseInventory; } } #endregion static Util() { } /// /// Are you sure prompt /// /// /// /// public static bool AreYouSure(string msg, string title) { if (MessageBox.Show( msg, title, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) return true; return false; } #region General helper methods static public Image AyaImage(string sImageResourceName) { if (AyaResource == null) return new Bitmap(10, 10);//this is strictly for design mode because forms based on setbase throw exception during design return (Image)AyaResource.GetObject(sImageResourceName); } static public Icon AyaIcon(string sImageResourceName) { return (Icon)AyaResource.GetObject(sImageResourceName); } static public void OpenWebURL(object oUrl) { if (oUrl == null) return; string sUrl = oUrl.ToString(); if (sUrl == "") return; if (sUrl.ToLower().StartsWith("http://")) System.Diagnostics.Process.Start(sUrl); else System.Diagnostics.Process.Start("http://" + sUrl); } /// /// Gets icon image from assembly /// /// /// static public Bitmap xImage(string ImageName) { // if(log.IsDebugEnabled) // //case 1039 //log.Debug("Image(" + ImageName + ")"); //string [] s=Assembly.GetExecutingAssembly().GetManifestResourceNames(); return new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("AyaNovaQBI." + ImageName)); } /// /// Invert a color so it's readable against the passed /// in color /// /// /// static public Color InvertColor(Color col) { // if(log.IsDebugEnabled) // //case 1039 //log.Debug("InvertColor("+col.ToString()+")"); int nSourceColor = col.R + col.G + col.B; int r = 255 - col.R; int g = 255 - col.G; int b = 255 - col.B; int nInvertColor = r + g + b; Color invert; if (nSourceColor - nInvertColor < 28) invert = Color.White; else invert = Color.FromArgb(r, g, b); return invert; } /// /// Checks to see if an item is linked already /// /// /// static public bool IsLinked(Guid AyaNovaID) { return (QBOIntegration.Maps[AyaNovaID] != null); } #endregion #region Authentication and connecting /* Our developer account info: Go to https://developer.intuit.com/ Sign in using: email: support@ayanova.com pw: i8BREAKfast! * */ static bool _AuthenticationCompleted = false; //SANDBOX KEYS //const string KEY_APP_TOKEN = "62925126b2b38b4c00b8e0bbcba877dbbceb"; //const string KEY_O_AUTH_CONSUMER = "qyprd7sqxwIgc5WZ2qT753CwUYwzM4"; //const string KEY_O_AUTH_CONSUMER_SECRET = "bEvWlrSgnQfdOh9JiGMRVeycM083KhZhTmX2Q9pW"; //XOR OBFUSCATED SANDBOX KEYS //const string KOAC = "8\rT8&\0P4D=TPz-:0Z"; //const string KAT = "\\O]\\RW\fD\rZ[\a}\rF_\v[\0y\f\f\v]~Y\r\v\0\0+"; //const string KOACS = "++\086.\0'\t\r,\rp$($13,\"Y[V,\a=={?O>"; //PRODUCTION KEYS //const string KEY_APP_TOKEN = "14fdc2bab8e1cb4e94b9168b0e44eee9bbdd"; //const string KEY_O_AUTH_CONSUMER = "qyprdvtMbWorJ5JuVce94uGEHxWvOM"; //const string KEY_O_AUTH_CONSUMER_SECRET = "H7IavvIL8jqpopx0vbBhDZRFBXxAHdABPo3IQSLZ"; //XOR OBFUSCATED PRODUCTION KEYS const string KOAC = "8\r\f!\0)P \f\fZQ<)3'4#"; const string KAT = "xZ\v\nQ\a(\fN\nX\0\a}\vO[\vZTV_\fWQ,\vV\v-"; const string KOACS = "Y?,V\f1^\0\r+\v!<0-1$\n7-9\fV\0?%#3"; //simple xor encryption private static string Ec(string text, string key) { var result = new StringBuilder(); for (int c = 0; c < text.Length; c++) result.Append((char)((uint)text[c] ^ (uint)key[c % key.Length])); return result.ToString(); } static ServiceContext SC = null; static DesktopIppOAuth.OAuthConnector CN; static public void StartAuthorization() { //#if(DEBUG) // string KEY_APP_TOKEN_ENCRYPTED = Ec(KEY_APP_TOKEN, "Invoice"); // string KEY_O_AUTH_CONSUMER_ENCRYPTED = Ec(KEY_O_AUTH_CONSUMER, "Invoice"); // string KEY_O_AUTH_CONSUMER_SECRET_ENCRYPTED = Ec(KEY_O_AUTH_CONSUMER_SECRET, "Invoice"); //#endif SC = null; CN = new OAuthConnector(); CN.IppOAuthResultEvent += _authResultEvent; //CN.Connect(KEY_O_AUTH_CONSUMER, KEY_O_AUTH_CONSUMER_SECRET, "http://www.ayanova.com"); CN.Connect(Ec(KOAC, "Invoice"), Ec(KOACS, "Invoice"), "http://www.ayanova.com"); } /// /// Process auth event /// /// /// /// /// static void _authResultEvent( string accessToken, string accessTokenSecret, string realmId, string dataSource) { //Intuit.Ipp.Security.OAuthRequestValidator oauthValidator = // new Intuit.Ipp.Security.OAuthRequestValidator( // accessToken, // accessTokenSecret, // KEY_O_AUTH_CONSUMER, // KEY_O_AUTH_CONSUMER_SECRET); Intuit.Ipp.Security.OAuthRequestValidator oauthValidator = new Intuit.Ipp.Security.OAuthRequestValidator( accessToken, accessTokenSecret, Ec(KOAC, "Invoice"), Ec(KOACS, "Invoice")); SC = new ServiceContext(accessToken, realmId, IntuitServicesType.QBO, oauthValidator); //TODO: set up a proper retry policy look into it //https://developer.intuit.com/docs/0100_quickbooks_online/0400_tools/0005_sdks/0010.net_tools/0050_retries //FOr now retry every 10 seconds for 3 times for total of a 30 seconds. #if(!DEBUG) //Can't do this because I lose the exception and the end user never sees it. // SC.IppConfiguration.RetryPolicy = new Intuit.Ipp.Retry.IntuitRetryPolicy(3, new TimeSpan(0, 0, 10)); #endif #if(DEBUG) //DANGER DANGER!!! If a retry is specified then the only exception error seen during dev is BadRequest //and not the actual error. //Have a case on the go about it: //https://help.developer.intuit.com/s/case/5000f00001Bu1umAAB //SC.IppConfiguration.RetryPolicy = new Intuit.Ipp.Retry.IntuitRetryPolicy(3, new TimeSpan(0, 0, 10)); //SC.IppConfiguration.Logger.RequestLog.EnableRequestResponseLogging = true; //SC.IppConfiguration.Logger.RequestLog.ServiceRequestLoggingLocation = @"C:\temp\QBOLOGS"; #endif //Notify that we are authenticated _AuthenticationCompleted = true; //todo what happens if we don't login?? //test case 3515 //System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; //Add this just to be sure to use TLS1.2 //var result = string.Empty; //using (var webClient = new System.Net.WebClient()) //{ // result = webClient.DownloadString("https://tlstest.intuit.com"); //} } #endregion authentication #region Pre flight check /// /// Pre flight check, prompt user as required to repair /// or add missing settings / failed ones /// /// /// /// public static pfstat PreFlightCheck() { if (!_AuthenticationCompleted) StartAuthorization(); while (!_AuthenticationCompleted) { //wait for authorization, //TODO: add timeout here } if (QBValidate() == pfstat.Cancel) { IntegrationLog.Log(QBID, "PFC: Unable to validate QuickBooks connection, user selected cancel"); return pfstat.Cancel; } else { IntegrationLog.Log(QBID, "PFC: QB validated Country=" + QCountry + ", QBVersion=" + QVersion.ToString() + ", Companyfile=" + QCompanyFile); PopulateQBListCache(); PopulateAyaListCache(); } IntegrationObjectCheck(); if (ValidateSettings(false) == pfstat.Cancel) { IntegrationLog.Log(QBID, "PFC: User settings not completed, user selected cancel"); return pfstat.Cancel; } //Added: 18-Nov-2006 CASE 163 //check that linked items in integration map exist in QB if (QBOIntegration.Maps.Count == 0) return pfstat.OK; //Missing links table: DataTable dtTemp = new DataTable(); dtTemp.Columns.Add("MAPID", typeof(Guid)); dtTemp.Columns.Add("Name", typeof(string)); bool present = true; foreach (IntegrationMap m in QBOIntegration.Maps) { present = true; switch (m.RootObjectType) { case RootObjectTypes.Client: present = QBClients.Rows.Contains(m.ForeignID); break; case RootObjectTypes.Vendor: present = QBVendors.Rows.Contains(m.ForeignID); break; case RootObjectTypes.Rate: case RootObjectTypes.Part: present = QBItems.Rows.Contains(m.ForeignID); break; } if (!present) dtTemp.Rows.Add(new object[] { m.ID, m.RootObjectType.ToString() + ": " + m.Name }); } if (dtTemp.Rows.Count > 0) { if (dtTemp.Rows.Count == QBOIntegration.Maps.Count) { //None of the items mapped match offer to remove them all #region Nothing matches IntegrationLog.Log(QBID, "PFC: No integration maps match qb database objects!"); DialogResult dr = MessageBox.Show("None of the mapped items in AyaNova were found in the \r\n" + "Currently open QuickBooks database.\r\n" + "It's possible you have the wrong database open.\r\n\r\n" + "Do you want to remove all mappings from AyaNova?", "", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button3); if (dr == DialogResult.Yes) { dr = MessageBox.Show("If you select YES all mappings will be removed from AyaNova.", "", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button3); if (dr == DialogResult.Yes) { IntegrationLog.Log(QBID, "PFC: User opted to remove all mappings after double warning."); foreach (DataRow row in dtTemp.Rows) { QBOIntegration.Maps.Remove(row["MAPID"].ToString()); } QBOIntegration = (Integration)QBOIntegration.Save(); return pfstat.Cancel; } } #endregion } else { //some items match so iterate them and offer to delete one by one IntegrationLog.Log(QBID, "PFC: Some integration maps do not match qb database objects"); foreach (DataRow row in dtTemp.Rows) { DialogResult dr = MessageBox.Show("Linked object: " + row["Name"].ToString() + "\r\n" + "Is missing or set Inactive in QuickBooks.\r\n\r\nRemove it's link from AyaNova?", "", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button3); if (dr == DialogResult.Cancel) return pfstat.Cancel; if (dr == DialogResult.Yes) QBOIntegration.Maps.Remove(row["MAPID"].ToString()); } QBOIntegration = (Integration)QBOIntegration.Save(); } } return pfstat.OK; } #region PFC AyaNova side /// /// Check if integration object is present in database, if not then /// add it. /// /// public static pfstat IntegrationObjectCheck() { if (QBOIntegration == null) { if (Integration.IntegrationExists(QBID)) { QBOIntegration = Integration.GetItem(QBID); //Get the QuickBooks settings object //Block below completely modified for //Case 299 QDat = new QBIDataEx(); if (QBOIntegration.AIObject == null || QBOIntegration.AIObject.ToString() == "") { QBOIntegration.AIObject = QDat.XMLData; QBOIntegration = (Integration)QBOIntegration.Save(); } else { if (QBOIntegration.AIObject is QBIData) { QDat.convertFromObjectFormat(QBOIntegration.AIObject); QBOIntegration.AIObject = QDat.XMLData; QBOIntegration = (Integration)QBOIntegration.Save(); } else { //All normal, parse xml and move on QDat.XMLData = (string)QBOIntegration.AIObject; } } //Old pre case 299 block // QDat = (QBIData)QBI.AIObject; //if (QDat == null) //{ // QDat = new QBIData(); // QBI.AIObject = QDat; // QBI = (Integration)QBI.Save(); //} } else { QBOIntegration = Integration.NewItem(QBID); QBOIntegration.Active = true; QBOIntegration.AppVersion = "7.x+"; QBOIntegration.Name = "AyaNova QBOI - QuickBooks Online integration"; //Case 299 QDat = new QBIDataEx(); QBOIntegration.AIObject = QDat.XMLData; QBOIntegration = (Integration)QBOIntegration.Save(); IntegrationLog.Log(QBID, "PFC: QBOI Integration object created"); } } return pfstat.OK; } #endregion #region PFC QB side /// /// Validate QB data /// public static pfstat QBValidate() { /* * Error reported by user: * 2017-12-09 17:24:09,339 [11032] FATAL AyaNova.Form1 - Unhandled exception System.ArgumentException: An item with the same key has already been added. at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource) at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) at AyaNova.PlugIn.QBOI.Util.QBValidate() at AyaNova.PlugIn.QBOI.Util.PreFlightCheck() * * * 2018-03-15 customer replied back finally with a duplicate key message: * Unexpected duplicate value returned by QuickBooks Online company info query Please copy this information to AyaNova technical support (support@ayanova.com) Company info returned: KEY: NeoEnabled, VALUE: true KEY: IsQbdtMigrated, VALUE: true KEY: MigrationDate, VALUE: Dec 9, 2017 4:42:19 AM KEY: MigrationSource, VALUE: QuickBooks-2015 KEY: IndustryType, VALUE: Computer Systems Design and Related Services KEY: CompanyType, VALUE: Limited Liability KEY: IndustryCode, VALUE: 5415 KEY: SubscriptionStatus, VALUE: PAID KEY: OfferingSku, VALUE: QuickBooks Online Essentials KEY: PayrollFeature, VALUE: false KEY: AccountantFeature, VALUE: false KEY: QBOIndustryType, VALUE: Manufacturing Businesses KEY: ItemCategoriesFeature, VALUE: true KEY: AssignedTime, VALUE: 12/08/2017 18:52:04 * NOTE that there are not actually any duplicates, so why did this happen??? * Does this data need to be cleared first before the attempt to fetch, is it a re-fetch causing this?? * Maybe this should be changed to show what it *thinks* are the dupes as well?? * * */ try { QueryService entityQuery = new QueryService(SC); List comp = entityQuery.ExecuteIdsQuery("SELECT * FROM CompanyInfo").ToList(); //case 3520 bool dupeKeyFoundInCompanyInfo = false; //Get the country and latest version supported //QB Online doesn't really have a "version", only a minor version //for the sake of compatibility just setting this to 3 which is the major api version number //current to when this code was written and probably best usage in the spirit of what this variable is all about QVersion = 3; foreach (Intuit.Ipp.Data.NameValue i in comp[0].NameValue) { //case 3520 if (QCompOtherPrefs.ContainsKey(i.Name)) { dupeKeyFoundInCompanyInfo = true; } else { QCompOtherPrefs.Add(i.Name, i.Value); } //get the exact type of QB Online if (i.Name == "OfferingSku") { QOfferingSku = i.Value; } } //case 3520 display issue if (dupeKeyFoundInCompanyInfo) { StringBuilder sMsg = new StringBuilder(); sMsg.AppendLine("Unexpected duplicate value returned by QuickBooks Online company info query"); sMsg.AppendLine("Please copy this information to AyaNova technical support (support@ayanova.com)"); sMsg.AppendLine("Company info returned:"); foreach (Intuit.Ipp.Data.NameValue i in comp[0].NameValue) { sMsg.AppendLine("KEY: " + i.Name + ", VALUE: " + i.Value); } CopyableMessageBox cp = new CopyableMessageBox(sMsg.ToString()); cp.ShowDialog(); } //this is current as of 7/27/2017 // wrong wrong wrong apparently, need to use preferences.ProductAndServicesPrefs.QuantityOnHand is true instead // QBSupportsInventory = (QOfferingSku == "QuickBooks Online Plus" || QOfferingSku == "QuickBooks Online"); QCountry = comp[0].Country;//In testing it was CA for canada and US for US, GB for UK, AU for australia QUSA = QCountry == "US"; //Get the company name (file was for desktop but keeping for JIC) QCompanyName = QCompanyFile = comp[0].CompanyName; return pfstat.OK; } catch (Exception ex) { IntegrationLog.Log(QBID, "PFC: Failed with exception:" + CrackException(ex)); //clean connection CN.Clean(); throw; } } #endregion #region PFC Check if user settings are completed and valid //case 3267 public static string TRANSACTION_CLASS_NO_CLASS_SELECTED = ""; /// /// Validate the users preferences /// if any are missing or invalid prompt for them /// /// public static pfstat ValidateSettings(bool ForceReset) { bool SetEverything = false; if (ForceReset) SetEverything = true; //Display user friendly dialog //explaining that configuration needs to be set if (QDat.NothingSet) { SetEverything = true; MessageBox.Show(new Form { TopMost = true }, "AyaNova QBOI has now connected sucessfully to both AyaNova and QuickBooks Online.\r\n\r\n" + "The next step is to set preferences for how AyaNova QBOI will operate.\r\n\r\n" + "AyaNova QBOI will now step through each setting and get your preference\r\n" + "in order to integrate AyaNova with QuickBooks.\r\n\r\n" + "These settings can be changed later.", "Setup wizard", MessageBoxButtons.OK, MessageBoxIcon.Information); } #region confirm company file ApproveCompanyFile s0 = new ApproveCompanyFile(); s0.QBCompanyName = QCompanyName.Replace("&", "&&"); s0.QBCompanyPath = QCompanyFile; if (s0.ShowDialog() == DialogResult.Cancel) { IntegrationLog.Log(QBID, "PFC: User cancelled when shown company file currently open - " + QCompanyFile); return pfstat.Cancel; } #endregion #region WO Pre status //Validate any existing status if (SetEverything == false && QDat.PreWOStatus != Guid.Empty) { if (WorkorderStatus.Exists(QDat.PreWOStatus)) goto PRESTATUSOK; } else { //Empty pre status is valid if not first //time setup as user can opt for selecting //workorders of any status if (SetEverything == false) goto PRESTATUSOK; } //We've arrived here because there is no valid setting for Pre workorder status //or it's the first time through and needs to be selected on way or another SetWOStatus s1 = new SetWOStatus(); s1.DialogTitle = "AyaNova QBOI setup - Choose billable Workorder Status"; s1.OptionTitle = "Billable workorder status"; s1.OptionDescription = "One of AyaNova QBOI's tasks is to look for work orders in AyaNova \r\n" + "that are ready to be billed out and put them in a list for your selection. \r\n" + " \r\n" + "By default QBOI will consider work orders that are set to service completed \r\n" + "and are not closed as it's selection criteria. \r\n" + " \r\n" + "In addition, you can further refine the types of work orders that QBOI \r\n" + "considers ready for billing by specifying here a particular workorder Status \r\n" + "you want to include in addition to the default criteria. "; s1.SelectedStatus = QDat.PreWOStatus; s1.PreStatus = true; if (s1.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.PreWOStatus = s1.SelectedStatus; PRESTATUSOK: #endregion #region WO POST status //Validate any existing status if (SetEverything == false && QDat.PostWOStatus != Guid.Empty) { if (WorkorderStatus.Exists(QDat.PostWOStatus)) goto POSTSTATUSOK; } else { //Empty post status is valid if not first //time setup if (SetEverything == false) goto POSTSTATUSOK; } //We've arrived here because there is no valid setting for POST workorder status //or it's the first time through and needs to be selected on way or another s1 = new SetWOStatus(); s1.DialogTitle = "AyaNova QBOI setup - Choose post billed Workorder Status"; s1.OptionTitle = "Post billing workorder status"; s1.OptionDescription = "After QBOI has billed out a work order, it can change the \r\n" + "work order status for you automatically if desired."; s1.SelectedStatus = QDat.PostWOStatus; s1.PreStatus = false; if (s1.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.PostWOStatus = s1.SelectedStatus; s1.Dispose(); s1 = null; POSTSTATUSOK: #endregion #region Outside service charge as //Validate any existing status if (SetEverything == false && QDat.OutsideServiceChargeAs != null && QDat.OutsideServiceChargeAs != "") { if (QBItems.Rows.Contains(QDat.OutsideServiceChargeAs)) goto OUTSIDESERVICECHARGEASOK; else { MessageBox.Show("The QuickBooks Item previously set for invoicing Outside Service items\r\n" + "No longer appears to be valid. You will next be prompted to re-select a valid \r\n" + "QuickBooks Item."); } } //We've arrived here because there is no valid setting for OutsideServiceChargeAs SetQBChargeAs s2 = new SetQBChargeAs(); s2.DialogTitle = "AyaNova QBOI setup - Charge outside service as?"; s2.OptionTitle = "Outside service"; s2.OptionDescription = "QBOI needs to know what QuickBooks Item you want \r\n" + "to use when invoicing the AyaNova \"Outside service\" portion of a work order.\r\n\r\n" + "Outside service is any 3rd party repair that is billable to the customer.\r\n\r\n" + "This setting is mandatory / required."; s2.QBItems = QBItems; s2.SelectedQBItem = QDat.OutsideServiceChargeAs; //s2.SelectedQBItem=QDat.PreWOStatus; if (s2.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.OutsideServiceChargeAs = s2.SelectedQBItem; s2.Dispose(); s2 = null; OUTSIDESERVICECHARGEASOK: #endregion #region Misc expense charge as //Validate any existing if (SetEverything == false && QDat.MiscExpenseChargeAs != null && QDat.MiscExpenseChargeAs != "") { //case 3292 uncommented the comment that had turned off this check if (QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) goto MISCCHARGEASOK; else { MessageBox.Show("The QuickBooks Item previously set for invoicing Misc. Expense items\r\n" + "No longer appears to be valid. You will next be prompted to re-select a valid \r\n" + "QuickBooks Item."); } } //We've arrived here because there is no valid setting for Misc expense s2 = new SetQBChargeAs(); s2.DialogTitle = "AyaNova QBOI setup - Charge Misc. Expense as?"; s2.OptionTitle = "Miscellaneous expenses"; s2.OptionDescription = "QBOI needs to know what QuickBooks Item you want \r\n" + "to use when invoicing the AyaNova \"Miscellaneous expense\" portion of a work order.\r\n\r\n" + "This setting is mandatory / required."; s2.QBItems = QBItems; s2.SelectedQBItem = QDat.MiscExpenseChargeAs; if (s2.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.MiscExpenseChargeAs = s2.SelectedQBItem; s2.Dispose(); s2 = null; MISCCHARGEASOK: #endregion #region Workorder item loan charge as //Validate any existing if (SetEverything == false && QDat.WorkorderItemLoanChargeAs != null && QDat.WorkorderItemLoanChargeAs != "") { //case 3292 uncommented the comment that had turned off this check if (QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) goto LOANCHARGEASOK; else { MessageBox.Show("The QuickBooks Item previously set for invoicing Misc. Expense items\r\n" + "No longer appears to be valid. You will next be prompted to re-select a valid \r\n" + "QuickBooks Item."); } } //We've arrived here because there is no valid setting for Misc expense s2 = new SetQBChargeAs(); s2.DialogTitle = "AyaNova QBOI setup - Charge loan item as?"; s2.OptionTitle = "Work order loans"; s2.OptionDescription = "QBOI needs to know what QuickBooks Item you want \r\n" + "to use when invoicing the AyaNova \"Workorder item loan\" portion of a work order.\r\n\r\n" + "This setting is mandatory / required."; s2.QBItems = QBItems; s2.SelectedQBItem = QDat.WorkorderItemLoanChargeAs; if (s2.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.WorkorderItemLoanChargeAs = s2.SelectedQBItem; s2.Dispose(); s2 = null; LOANCHARGEASOK: #endregion #region QB Transaction class //Validate any existing if (SetEverything == false && QDat.TransactionClass != null && QDat.TransactionClass != "") { //if something is set but there are no tr classes //then just clear it and move along if (QBClasses.Rows.Count == 1) { QDat.TransactionClass = TRANSACTION_CLASS_NO_CLASS_SELECTED;//case 3267 goto TRCLASSOK; } //Something is set and there *are* tr classes so //let's validate it... if (QBClasses.Rows.Contains(QDat.TransactionClass)) goto TRCLASSOK; else { MessageBox.Show("The QuickBooks transaction class previously set for invoicing\r\n" + "no longer appears to be valid. You will next be prompted to re-select it."); } } //Perhaps there are no transaction classes, this is the default //if not then don't prompt for it obviously :) //also if it was empty and were not in first setup mode then //don't bother prompting it might be the users choice. //todo: make something besides and empty string to indicate //deliberately non selected items //case 3228 commented out the following as it's a bad idea //if (QBClasses.Rows.Count == 1 || SetEverything == false) // goto TRCLASSOK; //We've arrived here because there is no setting for transaction classes //but there are some defined in QB SetQBClass s3 = new SetQBClass(); s3.DialogTitle = "AyaNova QBOI setup - Transaction class"; s3.OptionTitle = "Transaction class"; s3.OptionDescription = "QBOI needs to know what QuickBooks Transaction Class you want \r\n" + "to use when invoicing Work orders.\r\n\r\n" + "If you do not use transaction classes or are not sure what they are\r\n" + "select < Do not use classes> from the list below. Classes are off by default in QuickBooks.\r\n\r\n" + "This setting is Optional and not required."; s3.QBClasses = QBClasses; if (s3.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.TransactionClass = s3.SelectedQBClass; s3.Dispose(); s3 = null; TRCLASSOK: #endregion #region QB InvoiceTemplate //Templates are only supported in xml 3 or greater (all countries) //if Set everything (first run) then display a dialog about it if (QVersion < 3 && SetEverything == true) { SetInfoOnly s3a = new SetInfoOnly(); s3a.DialogTitle = "AyaNova QBOI setup - Invoice template"; s3a.OptionTitle = "Invoice template - NOT SUPPORTED"; s3a.OptionDescription = "QBOI can use a specific QuickBooks Invoice template for printing work orders.\r\n" + "However, your version of QuickBooks does not support integrating this \r\n" + "feature with 3rd party applications such as AyaNova QBOI.\r\n" + "Supported versions of QuickBooks for using Invoice templates with QBOI are:\r\n\r\n" + "U.S., Canadian or U.K. QuickBooks 2004 or newer\r\n\r\n" + "If you upgrade your QuickBooks in future you will be able to select this option\r\n" + "for now it is disabled and the default invoice template will be used"; s3a.ShowDialog(); goto TRInvoiceTemplateOK; } //Subsequent, non-setup, runs with unsupported version if (QVersion < 3) goto TRInvoiceTemplateOK; //Validate any existing if (QDat.QBInvoiceTemplate != null && QDat.QBInvoiceTemplate != "") { //if something is set but there are no InvoiceTemplates //then just clear it and move along if (QBInvoiceTemplates.Rows.Count == 1) { QDat.QBInvoiceTemplate = ""; goto TRInvoiceTemplateOK; } //Something is set and there *are* tr InvoiceTemplates so //let's validate it... if (QBInvoiceTemplates.Rows.Contains(QDat.QBInvoiceTemplate)) { if (!SetEverything) goto TRInvoiceTemplateOK; } else { MessageBox.Show("The QuickBooks Invoice Template previously set for invoicing\r\n" + "no longer appears to be valid. You will next be prompted to re-select it."); } } //Perhaps there are no InvoiceTemplates, this is the default //if not then don't prompt for it obviously :) //also if it was empty and were not in first setup mode then //don't bother prompting it might be the users choice. //todo: make something besides and empty string to indicate //deliberately non selected items if (QBInvoiceTemplates.Rows.Count == 1 || SetEverything == false) goto TRInvoiceTemplateOK; //We've arrived here because there is no setting for InvoiceTemplates //Or the user want's to change it //and there are some defined in QB SetQBInvoiceTemplate s3b = new SetQBInvoiceTemplate(); s3b.DialogTitle = "AyaNova QBOI setup - Invoice template"; s3b.OptionTitle = "Invoice template"; s3b.OptionDescription = "QBOI needs to know what QuickBooks Invoice template you want \r\n" + "QBOI to set for invoices created from Work orders.\r\n\r\n" + "QuickBooks Invoice templates are used in QuickBooks to specify different print formats\r\n" + "for invoices. If you do not use Invoice templates or are not sure what they are\r\n" + "select < Use default > from the list below.\r\n\r\n" + "This setting is required."; s3b.QBInvoiceTemplates = QBInvoiceTemplates; s3b.SelectedQBInvoiceTemplate = QDat.QBInvoiceTemplate; if (s3b.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.QBInvoiceTemplate = s3b.SelectedQBInvoiceTemplate; s3b.Dispose(); s3b = null; TRInvoiceTemplateOK: #endregion #region QB Terms //Validate any existing if (SetEverything == false && !string.IsNullOrEmpty(QDat.TermsDefault)) { //if something is set but there are no terms //then just clear it and move along if (QBTerms.Rows.Count == 1) { QDat.TermsDefault = ""; goto TermsOK; } //Something is set and there *are* terms so //let's validate it... if (QBTerms.Rows.Contains(QDat.TermsDefault)) { if (!SetEverything) goto TermsOK; } else { MessageBox.Show("The QuickBooks default terms previously set for invoicing\r\n" + "no longer appears to be valid. You will next be prompted to re-select it."); } } //We've arrived here because there is no setting for Terms //Or the user want's to change it //and there are some defined in QB SetQBTerms termsdialog = new SetQBTerms(); termsdialog.DialogTitle = "AyaNova QBOI setup - Customer default invoice terms"; termsdialog.OptionTitle = "Default terms"; termsdialog.OptionDescription = "QBOI needs to know what QuickBooks terms you want \r\n" + "QBOI to set for customers imported from AyaNova.\r\n\r\n" + "When an invoice for a customer is created the selected terms will be applied.\r\n\r\n" + "This setting is required."; termsdialog.QBTerms = QBTerms; termsdialog.SelectedQBTerm = QDat.TermsDefault; if (termsdialog.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.TermsDefault = termsdialog.SelectedQBTerm; termsdialog.Dispose(); termsdialog = null; TermsOK: #endregion #region ToBePrinted //No validation possible //so prompt only if not setup yet if (!SetEverything) { //if(QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) goto TBPOK; // else // { // MessageBox.Show("The QuickBooks Item previously set for invoicing Misc. Expense items\r\n" + // "No longer appears to be valid. You will next be prompted to re-select a valid \r\n" + // "QuickBooks Item."); // } } SetToBePrinted s4 = new SetToBePrinted(); s4.DialogTitle = "AyaNova QBOI setup - Set invoice to be printed?"; s4.OptionTitle = "Invoice to be printed"; s4.OptionDescription = "QBOI needs to know if you want invoices that it creates \r\n" + "in QuickBooks to be set to \"To be printed\".\r\n\r\n"; s4.ToBePrinted = QDat.ToBePrinted; if (s4.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.ToBePrinted = s4.ToBePrinted; s4.Dispose(); s4 = null; TBPOK: #endregion #region ToBePrinted //No validation possible //so prompt only if not setup yet if (!SetEverything) { goto TBEMOK; } SetToBeEmailed s4B = new SetToBeEmailed(); s4B.DialogTitle = "AyaNova QBOI setup - Set invoice to be emailed?"; s4B.OptionTitle = "Invoice to be emailed"; s4B.OptionDescription = "QBOI needs to know if you want invoices that it creates \r\n" + "in QuickBooks to be set to \"Send later\".\r\n\r\n"; s4B.ToBeEmailed = QDat.ToBeEmailed; if (s4B.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.ToBeEmailed = s4B.ToBeEmailed; s4B.Dispose(); s4B = null; TBEMOK: #endregion #region SetMemoField //No validation possible //so prompt only if not setup yet if (!SetEverything) { //if(QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) goto SETMEMOOK; // else // { // MessageBox.Show("The QuickBooks Item previously set for invoicing Misc. Expense items\r\n" + // "No longer appears to be valid. You will next be prompted to re-select a valid \r\n" + // "QuickBooks Item."); // } } SetMemoField s5 = new SetMemoField(); s5.DialogTitle = "AyaNova QBOI setup - Set Memo field?"; s5.OptionTitle = "Invoice memo field"; s5.OptionDescription = "QBOI needs to know if you want invoices that it creates \r\n" + "in QuickBooks to have their \"Memo\" field set with\r\n" + "information about the work order(s) that were the basis for\r\n" + "the invoice and the name of the AyaNova user who generated them.\r\n\r\n" + "This may be useful as a back reference, this setting is optional"; s5.FillMemoField = QDat.SetMemoField; if (s5.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.SetMemoField = s5.FillMemoField; s5.Dispose(); s5 = null; SETMEMOOK: #endregion #region SetAutoCloseField Case 7 //No validation possible //so prompt only if not setup yet if (!SetEverything) { goto SETAUTOCLOSEOK; } SetAutoClose s6 = new SetAutoClose(); s6.DialogTitle = "AyaNova QBOI setup - Close when invoiced?"; s6.OptionTitle = "Close work order after invoicing"; s6.OptionDescription = "QBOI needs to know if you want work orders that it invoices \r\n" + "automatically set to closed"; s6.AutoClose = QDat.AutoClose; if (s6.ShowDialog() == DialogResult.Cancel) { return pfstat.Cancel; } else QDat.AutoClose = s6.AutoClose; s6.Dispose(); s6 = null; SETAUTOCLOSEOK: #endregion //Save if changes made if (QDat.IsDirty) { //Case 299 QBOIntegration.AIObject = QDat.XMLData; //QBI.AIObject=QDat; QBOIntegration = (Integration)QBOIntegration.Save(); QDat.IsDirty = false; } return pfstat.OK; } #endregion #endregion pfc #region AyaNova cached lists public static void PopulateAyaListCache() { //Get the cached QB data Waiting w = new Waiting(); w.Show(); w.Ops = "Reading from AyaNova..."; w.Step = "Clients"; PopulateAyaClientList(); w.Step = "Vendors"; PopulateAyaVendorList(); w.Step = "Rates"; PopulateAyaRateList(); w.Step = "Parts"; PopulateAyaPartList(); w.Close(); } #region AyaNova clients private static ClientPickList _clientlist = null; /// /// AyaNova ClientPickList /// public static ClientPickList AyaClientList { get { return _clientlist; } } public static void PopulateAyaClientList() { _clientlist = ClientPickList.GetList(); } #endregion ayanova clients #region AyaNova Vendors private static VendorPickList _vendorlist = null; /// /// AyaNova vendor list /// public static VendorPickList AyaVendorList { get { return _vendorlist; } } public static void PopulateAyaVendorList() { _vendorlist = VendorPickList.GetList(); } #endregion ayanova vendors #region AyaNova Rates private static RatePickList _ratelist = null; /// /// AyaNova rate list /// public static RatePickList AyaRateList { get { return _ratelist; } } public static void PopulateAyaRateList() { _ratelist = RatePickList.GetListAllActiveRates(); } #endregion ayanova rates #region AyaNova Parts private static PartPickList _partlist = null; /// /// AyaNova part list /// public static PartPickList AyaPartList { get { return _partlist; } } public static void PopulateAyaPartList() { _partlist = PartPickList.GetAllParts(); } #endregion ayanova parts #endregion #region QB API helper methods/ attributes/cached lists /// /// Populate or repopulate the list of /// public static void PopulateQBListCache() { //Get the cached QB data Waiting w = new Waiting(); w.Show(); w.Ops = "Reading from QuickBooks Online..."; w.Step = "Company preferences"; PopulateQBPreferencesCache(); ////case 3294 / case 3296 //PopulateQBEntitlementCache(); w.Step = "Classes"; PopulateQBClassCache(); w.Step = "Vendors"; PopulateQBVendorCache(); w.Step = "Customers"; PopulateQBClientCache(); w.Step = "Items"; PopulateQBItemCache(); if (!(QVersion < 3))//qbXML 3.0 or higher (QB 2004 any country or newer) { w.Step = "Invoice templates"; PopulateQBInvoiceTemplates(); } //case 632 w.Step = "Accounts"; PopulateQBAccountCache(); //case 519 w.Step = "Terms"; PopulateQBTermsCache(); w.Step = "Tax codes"; PopulateQBTaxCodesCache(); w.Close(); } #region QuickBooks "items" public enum qbitemtype { Inventory, NonInventory, Service, OtherCharge, Assembly } private static DataTable _dtQBItems = null; /// /// qb items /// public static DataTable QBItems { get { return _dtQBItems; } } /// /// Given a QB Item ID, return the /// AyaNova Vendor ID linked to that items /// QB preferred Vendor ID or /// Guid empty on any problem or not found /// /// /// public static Guid AyaVendorForQBItem(string QBItemID) { if (QBItemID == null || QBItemID == "") return Guid.Empty; DataRow dr = _dtQBItems.Rows.Find(QBItemID); if (dr == null || dr["VendorID"] == null || dr["VendorID"].ToString() == "") return Guid.Empty; DataRow drVendor = _dtQBVendors.Rows.Find(dr["VendorID"].ToString()); if (drVendor == null) return Guid.Empty; if (!QBOIntegration.Maps.Contains(drVendor["ID"].ToString(), RootObjectTypes.Vendor)) return Guid.Empty; //Ok we have a matching vendor in the list, return the guid of it return QBOIntegration.Maps[drVendor["ID"].ToString(), RootObjectTypes.Vendor].RootObjectID; } /// /// Populate the cached qb data /// billable /// private static void PopulateQBItemCache() { if (_dtQBItems == null) { _dtQBItems = new DataTable("QBItems"); //setup the columns _dtQBItems.Columns.Add("ID", typeof(string)); _dtQBItems.Columns.Add("FullName", typeof(string)); _dtQBItems.Columns.Add("Type", typeof(qbitemtype)); _dtQBItems.Columns.Add("Modified", typeof(DateTime)); _dtQBItems.Columns.Add("Price", typeof(decimal)); _dtQBItems.Columns.Add("Cost", typeof(decimal)); _dtQBItems.Columns.Add("SalesDesc", typeof(string)); _dtQBItems.Columns.Add("ReorderPoint", typeof(decimal)); _dtQBItems.Columns.Add("VendorID", typeof(string)); _dtQBItems.Columns.Add("Taxable", typeof(bool)); _dtQBItems.Columns.Add("TaxId", typeof(string));//non US locales _dtQBItems.PrimaryKey = new DataColumn[] { _dtQBItems.Columns[0] }; //Case 237 _dtQBItems.DefaultView.Sort = "FullName asc"; } else _dtQBItems.Clear(); //Assembly is coming back invalid and they have the bundle feature (known as Group in the api) so going with that for now string[] qboItemTypes = { "Service", "Inventory", "NonInventory", "Group" }; foreach (string s in qboItemTypes) { try { qbitemtype itemType = qbitemtype.OtherCharge; switch (s) { case "Service": itemType = qbitemtype.Service; break; case "Inventory": itemType = qbitemtype.Inventory; break; case "NonInventory": itemType = qbitemtype.NonInventory; break; case "Group": itemType = qbitemtype.Assembly; break; } bool bDone = false; int nStart = 0; QueryService entityQuery = new QueryService(SC); while (!bDone) { //GET first 1000 List items = entityQuery.ExecuteIdsQuery("select * from Item where Type='" + s + "' startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; string salesTaxId = string.Empty; foreach (q.Item i in items) { if (i.SalesTaxCodeRef != null) salesTaxId = i.SalesTaxCodeRef.Value; else salesTaxId = string.Empty; _dtQBItems.Rows.Add( new object[] { i.Id, i.Name, itemType, i.MetaData.LastUpdatedTime, i.UnitPrice, i.PurchaseCost, i.Description, i.ReorderPoint, "",//QBO doesn't support vendor id property i.Taxable, salesTaxId }); } //REPEAT } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBItems: Failed with exception:" + CrackException(ex)); throw; } } } #endregion quickbooks items #region QuickBooks "Transactionclasses" private static DataTable _dtQBClasses = null; /// /// QB Transaction Classes /// public static DataTable QBClasses { get { return _dtQBClasses; } } /// /// Populate the cached qb data /// billable /// private static void PopulateQBClassCache() { if (_dtQBClasses == null) { _dtQBClasses = new DataTable("QBClasses"); //setup the columns _dtQBClasses.Columns.Add("ID", typeof(string)); _dtQBClasses.Columns.Add("FullName", typeof(string)); _dtQBClasses.PrimaryKey = new DataColumn[] { _dtQBClasses.Columns[0] }; //Case 237 _dtQBClasses.DefaultView.Sort = "FullName asc"; } else _dtQBClasses.Clear(); //case 3267 _dtQBClasses.Rows.Add(new object[] { TRANSACTION_CLASS_NO_CLASS_SELECTED, "< Do not use classes >" }); try { bool bDone = false; int nStart = 0; QueryService classQuery = new QueryService(SC); while (!bDone) { //GET first 1000 List classes = classQuery.ExecuteIdsQuery("select name from Class startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (classes.Count < 999) bDone = true; else nStart += 1000; foreach (q.Class i in classes) { //add a record to the datatable _dtQBClasses.Rows.Add(new object[] { i.Id, i.Name }); } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBClasses: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks transaction classes #region QuickBooks Invoice Templates private static DataTable _dtQBInvoiceTemplates = null; /// /// QB Transaction Templates /// public static DataTable QBInvoiceTemplates { get { return _dtQBInvoiceTemplates; } } /// /// Populate the cached qb data /// Invoice templates /// private static void PopulateQBInvoiceTemplates() { if (_dtQBInvoiceTemplates == null) { _dtQBInvoiceTemplates = new DataTable("QBInvoiceTemplates"); //setup the columns _dtQBInvoiceTemplates.Columns.Add("ID", typeof(string)); _dtQBInvoiceTemplates.Columns.Add("FullName", typeof(string)); _dtQBInvoiceTemplates.PrimaryKey = new DataColumn[] { _dtQBInvoiceTemplates.Columns[0] }; //Case 237 _dtQBInvoiceTemplates.DefaultView.Sort = "FullName asc"; } else _dtQBInvoiceTemplates.Clear(); _dtQBInvoiceTemplates.Rows.Add(new object[] { "", "< Use default (currently not supported by Online QB) >" }); try { //TODO: There appear to be no invoice templates available through the UI } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBInvoiceTemplates: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks Templates #region QuickBooks "Customers" #region SyncToken fetching private static string GetQBCustomerSyncToken(string customerid) { try { QueryService qs = new QueryService(SC); List items = qs.ExecuteIdsQuery("select synctoken from Customer where id = '" + customerid + "'").ToList(); if (items.Count < 1) return string.Empty; else return items[0].SyncToken; } catch (Exception ex) { IntegrationLog.Log(QBID, "GetQBCustomerSyncToken: Failed with exception:" + CrackException(ex)); throw; } } #endregion SyncToken private static DataTable _dtQBClients = null; /// /// QB Transaction Clients /// public static DataTable QBClients { get { return _dtQBClients; } } #region Address structure /// /// Address properties /// public struct Address { public string DeliveryAddress; public string City; public string StateProv; public string Country; public string Postal; } #endregion /// /// Populate the cached qb data /// of customers / clients /// private static void PopulateQBClientCache() { if (_dtQBClients == null) { _dtQBClients = new DataTable("QBClients"); //setup the columns _dtQBClients.Columns.Add("ID", typeof(string)); _dtQBClients.Columns.Add("FullName", typeof(string)); _dtQBClients.Columns.Add("MailAddress", typeof(Address)); _dtQBClients.Columns.Add("StreetAddress", typeof(Address)); _dtQBClients.Columns.Add("Phone", typeof(string)); _dtQBClients.Columns.Add("Fax", typeof(string)); _dtQBClients.Columns.Add("AltPhone", typeof(string)); _dtQBClients.Columns.Add("Email", typeof(string)); _dtQBClients.Columns.Add("Contact", typeof(string)); _dtQBClients.Columns.Add("Created", typeof(DateTime)); _dtQBClients.Columns.Add("Modified", typeof(DateTime)); _dtQBClients.Columns.Add("Account", typeof(string)); _dtQBClients.Columns.Add("Taxable", typeof(bool)); _dtQBClients.Columns.Add("TaxId", typeof(string)); //Need to get company wide default tax code then set it for each //customer that is taxable yes, but has no default tax code set //or maybe it is set by the .net sdk, have to test that //i.DefaultTaxCodeRef will be null but taxable can be true (amys bird sanctuary example) //_dtQBClients.Columns.Add("TaxCode", typeof(string)); _dtQBClients.PrimaryKey = new DataColumn[] { _dtQBClients.Columns[0] }; //Case 237 _dtQBClients.DefaultView.Sort = "FullName asc"; } else _dtQBClients.Clear(); try { bool bDone = false; int nStart = 0; QueryService qs = new QueryService(SC); while (!bDone) { //GET first 1000 List items = qs.ExecuteIdsQuery("select * from Customer startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; foreach (q.Customer i in items) { //add a record to the datatable // _dtQBClients.Columns.Add("Taxable", typeof(bool)); //_dtQBClients.Columns.Add("TaxId", typeof(string)); string sTaxCode = TAX_CODE_ID_NO_TAX; if (i.Taxable) { if (i.DefaultTaxCodeRef != null) { sTaxCode = i.DefaultTaxCodeRef.Value; } else { //Use company pref tax code if (QBPreferences.TaxPrefs.AnyIntuitObject != null) { sTaxCode = _QBPreferences.TaxPrefs.AnyIntuitObject.Value; } else { //There is no company tax code and no customer tax code so we'll just accept the //already set default of TAX_CODE_ID_NO_TAX } // QBPreferences.TaxPrefs..TaxPrefs. } } //case 3565 (related to case 3520) if (!_dtQBClients.Rows.Contains(i.Id)) { _dtQBClients.Rows.Add( new object[]{ i.Id, i.DisplayName, ProcessAddress(i.BillAddr), ProcessAddress(i.ShipAddr), ProcessQBPhone(i.PrimaryPhone), ProcessQBPhone(i.Fax), ProcessQBPhone(i.AlternatePhone), ProcessQBEmail(i.PrimaryEmailAddr), i.ContactName, i.MetaData.CreateTime, i.MetaData.LastUpdatedTime, i.AcctNum, i.Taxable, sTaxCode }); } } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBClients: Failed with exception:" + CrackException(ex)); throw; } } /// /// handle null phones, just get the digits /// /// /// private static string ProcessQBPhone(q.TelephoneNumber ph) { if (ph == null) return string.Empty; else return ph.FreeFormNumber; } /// /// handle null phones, just get the digits /// /// /// private static string ProcessQBEmail(q.EmailAddress em) { if (em == null) return string.Empty; else return em.Address; } /// /// Take a qb address and return an AyaNova friendly /// address structure /// /// /// private static Address ProcessAddress(q.PhysicalAddress a) { Address b = new Address(); b.City = ""; b.Country = ""; b.DeliveryAddress = ""; b.Postal = ""; b.StateProv = ""; if (a == null) return b; //Append each line of the address, add cr/lf for each line if present after //the first line //Assumption: First line always has *something* in it b.DeliveryAddress = a.Line1; b.DeliveryAddress += AyaBizUtils.SS("\r\n", a.Line2, ""); b.DeliveryAddress += AyaBizUtils.SS("\r\n", a.Line3, ""); b.DeliveryAddress += AyaBizUtils.SS("\r\n", a.Line4, ""); b.DeliveryAddress += AyaBizUtils.SS("\r\n", a.Line5, ""); b.City = a.City; b.StateProv = a.CountrySubDivisionCode; b.Country = a.Country; b.Postal = a.PostalCode; if (b.DeliveryAddress == null) b.DeliveryAddress = string.Empty; if (b.City == null) b.City = string.Empty; if (b.StateProv == null) b.StateProv = string.Empty; if (b.Country == null) b.Country = string.Empty; if (b.Postal == null) b.Postal = string.Empty; return b; } #endregion quickbooks transaction Clients #region QuickBooks "Vendors" private static DataTable _dtQBVendors = null; /// /// QB Vendors /// public static DataTable QBVendors { get { return _dtQBVendors; } } /// /// Populate the cached qb data /// of Vendors /// private static void PopulateQBVendorCache() { if (_dtQBVendors == null) { _dtQBVendors = new DataTable("QBVendors"); //setup the columns _dtQBVendors.Columns.Add("ID", typeof(string)); _dtQBVendors.Columns.Add("FullName", typeof(string)); _dtQBVendors.Columns.Add("MailAddress", typeof(Address)); _dtQBVendors.Columns.Add("StreetAddress", typeof(Address)); _dtQBVendors.Columns.Add("Phone", typeof(string)); _dtQBVendors.Columns.Add("Fax", typeof(string)); _dtQBVendors.Columns.Add("AltPhone", typeof(string)); _dtQBVendors.Columns.Add("Email", typeof(string)); _dtQBVendors.Columns.Add("Contact", typeof(string)); _dtQBVendors.Columns.Add("Created", typeof(DateTime)); _dtQBVendors.Columns.Add("Modified", typeof(DateTime)); _dtQBVendors.Columns.Add("Account", typeof(string)); _dtQBVendors.PrimaryKey = new DataColumn[] { _dtQBVendors.Columns[0] }; //Case 237 _dtQBVendors.DefaultView.Sort = "FullName asc"; } else _dtQBVendors.Clear(); try { bool bDone = false; int nStart = 0; QueryService qs = new QueryService(SC); while (!bDone) { //GET first 1000 List items = qs.ExecuteIdsQuery("select * from Vendor startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; foreach (q.Vendor i in items) { _dtQBVendors.Rows.Add( new object[]{ i.Id, i.DisplayName, ProcessAddress(i.BillAddr), ProcessAddress(i.ShipAddr), ProcessQBPhone(i.PrimaryPhone), ProcessQBPhone(i.Fax), ProcessQBPhone(i.AlternatePhone), ProcessQBEmail(i.PrimaryEmailAddr), i.ContactName, i.MetaData.CreateTime, i.MetaData.LastUpdatedTime, i.AcctNum }); } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBVendors: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks Vendors #region QuickBooks "accounts" private static DataTable _dtQBAccounts = null; /// /// QB Transaction Classes /// public static DataTable QBAccounts { get { return _dtQBAccounts; } } /// /// Populate the cached qb account list data /// private static void PopulateQBAccountCache() { if (_dtQBAccounts == null) { _dtQBAccounts = new DataTable("QBAccounts"); //setup the columns _dtQBAccounts.Columns.Add("ID", typeof(string)); _dtQBAccounts.Columns.Add("FullName", typeof(string)); _dtQBAccounts.Columns.Add("Type", typeof(string)); _dtQBAccounts.PrimaryKey = new DataColumn[] { _dtQBAccounts.Columns[0] }; //Case 237 _dtQBAccounts.DefaultView.Sort = "FullName asc"; } else _dtQBAccounts.Clear(); try { bool bDone = false; int nStart = 0; QueryService qs = new QueryService(SC); while (!bDone) { //GET first 1000 List items = qs.ExecuteIdsQuery("select * from Account startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; foreach (q.Account i in items) { //add a record to the datatable _dtQBAccounts.Rows.Add( new object[] { i.Id, i.AccountType + " - " + i.Name, i.AccountType }); } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBAccounts: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks accounts #region QuickBooks "Terms" private static DataTable _dtQBTerms = null; /// /// QB terms /// public static DataTable QBTerms { get { return _dtQBTerms; } } /// /// Populate the cached qb terms list data /// private static void PopulateQBTermsCache() { if (_dtQBTerms == null) { _dtQBTerms = new DataTable("QBTerms"); //setup the columns _dtQBTerms.Columns.Add("ID", typeof(string)); _dtQBTerms.Columns.Add("FullName", typeof(string)); _dtQBTerms.PrimaryKey = new DataColumn[] { _dtQBTerms.Columns[0] }; //Case 237 _dtQBTerms.DefaultView.Sort = "FullName asc"; } else _dtQBTerms.Clear(); try { bool bDone = false; int nStart = 0; QueryService qs = new QueryService(SC); while (!bDone) { //GET first 1000 List items = qs.ExecuteIdsQuery("select * from Term startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; foreach (q.Term i in items) { //add a record to the datatable _dtQBTerms.Rows.Add( new object[] { i.Id, i.Name }); } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBTerms: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks Terms #region QuickBooks "TaxCodes" private static DataTable _dtQBTaxCodes = null; /// /// QB tax codes /// public static DataTable QBTaxCodes { get { return _dtQBTaxCodes; } } public static string TAX_CODE_ID_NO_TAX = ""; /// /// Populate the cached qb list data /// private static void PopulateQBTaxCodesCache() { if (_dtQBTaxCodes == null) { _dtQBTaxCodes = new DataTable("QBTaxCodes"); //setup the columns _dtQBTaxCodes.Columns.Add("ID", typeof(string)); _dtQBTaxCodes.Columns.Add("FullName", typeof(string)); _dtQBTaxCodes.PrimaryKey = new DataColumn[] { _dtQBTaxCodes.Columns[0] }; //Case 237 _dtQBTaxCodes.DefaultView.Sort = "FullName asc"; } else _dtQBTaxCodes.Clear(); try { bool bDone = false; int nStart = 0; QueryService qs = new QueryService(SC); if (QUSA) { _dtQBTaxCodes.Rows.Add( new object[] { TAX_CODE_ID_NO_TAX, "< NOT TAXABLE >" }); } while (!bDone) { //GET first 1000 List items = qs.ExecuteIdsQuery("select * from TaxCode startposition " + nStart.ToString() + " maxresults 1000").ToList(); //IF WE GOT 1000 THEN GET THE NEXT 1000 if (items.Count < 999) bDone = true; else nStart += 1000; foreach (q.TaxCode i in items) { //add a record to the datatable _dtQBTaxCodes.Rows.Add( new object[] { i.Id, i.Name }); } } } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBTaxCodes: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks TaxCodes #region QuickBooks "Preferences" private static q.Preferences _QBPreferences = null; public static q.Preferences QBPreferences { get { return _QBPreferences; } } //this is how you're supposed to check if inventory is enabled in this edition //https://help.developer.intuit.com/s/question/0D50f00004n4Srg/what-are-the-possible-values-of-offeringsku-in-companyinfo-api-i-could-see-the-values-quickbooks-online-plus-and-quickbooks-plus-for-offeringsku-is-there-any-difference-between-them-if-yes-what-is-the-difference?s1oid=00DG0000000COk8&OpenCommentForEdit=1&s1nid=0DBG0000000blK7&emkind=chatterCommentNotification&s1uid=0050f000008K29c&emtm=1501196262050&fromEmail=1&s1ext=0 public static bool QBSupportsInventory { get { return _QBPreferences.ProductAndServicesPrefs.QuantityOnHand; } } /// /// Populate the cached qb data /// private static void PopulateQBPreferencesCache() { try { QueryService qs = new QueryService(SC); //GET the one item List items = qs.ExecuteIdsQuery("select * from preferences").ToList(); _QBPreferences = items[0]; } catch (Exception ex) { IntegrationLog.Log(QBID, "PopulateQBPreferences: Failed with exception:" + CrackException(ex)); throw; } } #endregion quickbooks TaxCodes #region QuickBooks Online "Entitlements" ////Added for case 3294 / case 3296 //private static q.Entitlement _QBEntitlement = null; //public static q.Entitlement QBEntitlement //{ // get // { // return _QBEntitlement; // } //} ///// ///// Populate the cached qb data ///// //private static void PopulateQBEntitlementCache() //{ // try // { // QueryService qs = new QueryService(SC); // //GET the one item // List items = qs.ExecuteIdsQuery("select * from entitlements").ToList(); // _QBEntitlement = items[0]; // } // catch (Exception ex) // { // IntegrationLog.Log(QBID, "PopulateQBEntitlement: Failed with exception:" + CrackException(ex)); // throw; // } //} #endregion quickbooks TaxCodes #endregion #region Import Export/refresh #region Import / refresh to AyaNova #region Import / refresh customer #region Refresh Customer public static void RefreshAyaNovaClientFromQB(List objectIDList) { PopulateQBClientCache(); foreach (Guid g in objectIDList) { try { Client c = Client.GetItemNoMRU(g); RefreshAyaNovaClientFromQB(c); if (c.IsSavable) c.Save(); } catch { }; } } public static void RefreshAyaNovaClientFromQB(Client c) { PopulateQBClientCache(); IntegrationMap im = QBOIntegration.Maps[c.ID]; if (im == null) return;//this client is not linked DataRow dr = _dtQBClients.Rows.Find(im.ForeignID); if (dr == null) return; //QBListID not found in client list? CopyQBCustomerInfoToAyaNovaClient(dr, c); string sName = dr["FullName"].ToString(); if (sName.Length > 255) sName = sName.Substring(0, 255); c.Name = sName; } #endregion refresh customer #region Import customer /// /// Import the indicated customer /// to an AyaNova client record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportQBCustomer(string QuickBooksID, ArrayList alErrors) { DataRow dr = _dtQBClients.Rows.Find(QuickBooksID); //QBListID not found in client list? if (dr == null) { alErrors.Add("ImportQBCustomer: ID not found " + QuickBooksID); return; } string sName = dr["FullName"].ToString(); if (sName.Length > 255) { alErrors.Add("ImportQBCustomer: QuickBooks customer name exceeds 255 character limit for AyaNova\r\n" + "Name: " + dr["FullName"].ToString() + "\r\n" + "was imported as: " + sName); sName = sName.Substring(0, 255); } try { //already a client by that name if (Client.Exists(Guid.Empty, dr["FullName"].ToString())) { alErrors.Add("ImportQBCustomer: " + dr["FullName"].ToString() + " already exists in AyaNova"); return; } //Import seems safe... Client c = Client.NewItem(); c.Name = sName;//1 CopyQBCustomerInfoToAyaNovaClient(dr, c); if (!c.IsSavable) { alErrors.Add("ImportQBCustomer: AyaNova won't allow import of " + c.Name + "\r\n" + "Due to the following broken rules:\r\n" + c.GetBrokenRulesString()); return; } c = (Client)c.Save(); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = sName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Client; m.LastSync = DateTime.Now; m.ForeignID = QuickBooksID; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { //crack the exception in case it's a generic dataportal one //and it is if it's got an inner exception of any kind alErrors.Add("ImportQBCustomer: AyaNova won't allow import / link of " + sName + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion Import customer #region Copy QB Customer info to AyaNova client public static void CopyQBCustomerInfoToAyaNovaClient(DataRow dr, Client c) { Address a = (Address)dr["MailAddress"]; if (!string.IsNullOrEmpty(a.DeliveryAddress)) c.MailToAddress.DeliveryAddress = a.DeliveryAddress;//2 if (!string.IsNullOrEmpty(a.City)) c.MailToAddress.City = a.City;//3 if (!string.IsNullOrEmpty(a.StateProv)) c.MailToAddress.StateProv = a.StateProv;//4 if (!string.IsNullOrEmpty(a.Country)) c.MailToAddress.Country = a.Country;//5 if (!string.IsNullOrEmpty(a.Postal)) c.MailToAddress.Postal = a.Postal;//6 a = (Address)dr["StreetAddress"]; if (!string.IsNullOrEmpty(a.DeliveryAddress)) c.GoToAddress.DeliveryAddress = a.DeliveryAddress;//7 if (!string.IsNullOrEmpty(a.City)) c.GoToAddress.City = a.City;//8 if (!string.IsNullOrEmpty(a.StateProv)) c.GoToAddress.StateProv = a.StateProv;//9 if (!string.IsNullOrEmpty(a.Country)) c.GoToAddress.Country = a.Country;//10 if (!string.IsNullOrEmpty(a.Postal)) c.GoToAddress.Postal = a.Postal;//11 //Case 518 c.PopulateBothAddresses(); //Contact cn=c.Contacts.Add(RootObjectTypes.Client,c.ID); if (!string.IsNullOrEmpty(dr["Contact"].ToString())) c.Contact = dr["Contact"].ToString(); //Phone field if (!string.IsNullOrEmpty(dr["Phone"].ToString())) { c.Phone1 = dr["Phone"].ToString(); } //Fax field if (!string.IsNullOrEmpty(dr["Fax"].ToString())) { c.Phone2 = dr["Fax"].ToString();//15 } //AltPhone field if (!string.IsNullOrEmpty(dr["AltPhone"].ToString())) { c.Phone3 = dr["AltPhone"].ToString();//16 } //Email field if (!string.IsNullOrEmpty(dr["Email"].ToString())) { c.Email = dr["Email"].ToString();//17 } //Account number field if (!string.IsNullOrEmpty(dr["Account"].ToString())) { c.AccountNumber = dr["Account"].ToString();//18 } } #endregion copy qb customer info to aya client #endregion import refresh customer #region Vendor /// /// Import the indicated Vendor /// to an AyaNova vendor record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportQBVendor(string QuickBooksID, VendorTypes AsVendorType, ArrayList alErrors) { DataRow dr = _dtQBVendors.Rows.Find(QuickBooksID); //QBListID not found in Vendor list? if (dr == null) { alErrors.Add("ImportQBVendor: ID not found " + QuickBooksID); return; } string sName = dr["FullName"].ToString(); if (sName.Length > 255) { alErrors.Add("ImportQBVendor: QuickBooks Vendor name exceeds 255 character limit for AyaNova\r\n" + "Name: " + dr["FullName"].ToString() + "\r\n" + "was imported as: " + sName); sName = sName.Substring(0, 255); } try { //already a Vendor by that name if (Vendor.Exists(Guid.Empty, dr["FullName"].ToString())) { alErrors.Add("ImportQBVendor: " + dr["FullName"].ToString() + " already exists in AyaNova"); return; } //Import seems safe... Vendor c = Vendor.NewItem(); c.VendorType = AsVendorType; c.Name = sName; Address a = (Address)dr["MailAddress"]; c.MailToAddress.DeliveryAddress = a.DeliveryAddress; c.MailToAddress.City = a.City; c.MailToAddress.StateProv = a.StateProv; c.MailToAddress.Country = a.Country; c.MailToAddress.Postal = a.Postal; a = (Address)dr["StreetAddress"]; c.GoToAddress.DeliveryAddress = a.DeliveryAddress; c.GoToAddress.City = a.City; c.GoToAddress.StateProv = a.StateProv; c.GoToAddress.Country = a.Country; c.GoToAddress.Postal = a.Postal; //Contact cn=c.Contacts.Add(RootObjectTypes.Vendor,c.ID); c.Contact = dr["Contact"].ToString(); ////if it's completely empty we'll substitute the word Contact instead //if(qbcontact=="") // cn.FirstName="Contact"; //else //{ // //default it first // cn.FirstName=qbcontact; // string [] contactnames=null; // if(qbcontact.IndexOf(" ")!=-1) // contactnames=qbcontact.Replace(",","").Split(' ');//replace any commas if present // else // if(qbcontact.IndexOf(",")!=-1) // contactnames=qbcontact.Split(','); // //Quickbooks just has one field for contact so // //we'll assume the english speaking tradition of firstname space lastname // //if there is a space otherwise we'll assume it's just a first name if there are no spaces in it // if(contactnames!=null && contactnames.GetLength(0)>1) // { // cn.FirstName=contactnames[0]; // cn.LastName=contactnames[1]; // } //} //Phone field if (dr["Phone"].ToString() != "") { //ContactPhone cp=cn.Phones.Add(cn); //cp.ContactPhoneType=ContactPhoneTypes.Business; c.Phone1 = dr["Phone"].ToString(); //case 124 //cp.PhoneDefault=true; } //Fax field if (dr["Fax"].ToString() != "") { //ContactPhone cp=cn.Phones.Add(cn); //cp.ContactPhoneType=ContactPhoneTypes.Fax; c.Phone2 = dr["Fax"].ToString(); } //AltPhone field if (dr["AltPhone"].ToString() != "") { //ContactPhone cp=cn.Phones.Add(cn); //cp.ContactPhoneType=ContactPhoneTypes.Business; c.Phone3 = dr["AltPhone"].ToString(); } //Email field if (dr["Email"].ToString() != "") { c.Email = dr["Email"].ToString(); } //Account number if (dr["Account"].ToString() != "") { c.AccountNumber = dr["Account"].ToString(); } if (!c.IsSavable) { alErrors.Add("ImportQBVendor: AyaNova won't allow import of " + c.Name + "\r\n" + "Due to the following broken rules:\r\n" + c.GetBrokenRulesString()); return; } c = (Vendor)c.Save(); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = sName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Vendor; m.LastSync = DateTime.Now; m.ForeignID = QuickBooksID; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportQBVendor: AyaNova won't allow import / link of " + sName + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion #region Rate /// /// Import the indicated QB Item /// to an AyaNova Rate record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportQBRate(string QuickBooksID, RateTypes AsRateType, Guid MostLikelyRateUnitChargeDescriptionID, ArrayList alErrors) { DataRow dr = _dtQBItems.Rows.Find(QuickBooksID); //QBListID not found in Rate list? if (dr == null) { alErrors.Add("ImportQBRate: ID not found " + QuickBooksID); return; } string sName = dr["FullName"].ToString(); if (sName.Length > 255) { alErrors.Add("ImportQBRate: QuickBooks Rate name exceeds 255 character limit for AyaNova\r\n" + "Name: " + dr["FullName"].ToString() + "\r\n" + "was imported as: " + sName); sName = sName.Substring(0, 255); } try { //already a Rate by that name if (Rate.Exists(Guid.Empty, dr["FullName"].ToString())) { alErrors.Add("ImportQBRate: " + dr["FullName"].ToString() + " already exists in AyaNova"); return; } //Import seems safe... Rates rates = Rates.GetItems(false); Rate c = rates.Add(); c.RateType = AsRateType; c.Name = sName; c.Charge = (decimal)dr["Price"]; c.ContractRate = false; c.Cost = (decimal)dr["Cost"]; c.Description = T(255, dr["SalesDesc"].ToString()); c.RateUnitChargeDescriptionID = MostLikelyRateUnitChargeDescriptionID; if (!c.IsSavable) { alErrors.Add("ImportQBRate: AyaNova won't allow import of " + c.Name + "\r\n" + "Due to the following broken rules:\r\n" + c.GetBrokenRulesString()); return; } rates = (Rates)rates.Save(); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = sName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Rate; m.LastSync = DateTime.Now; m.ForeignID = QuickBooksID; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportQBRate: AyaNova won't allow import / link of " + sName + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion #region Part public static void RefreshAyaNovaPartFromQB(List objectIDList) { PopulateQBItemCache(); foreach (Guid g in objectIDList) { try { Part c = Part.GetItem(g); RefreshAyaNovaPartFromQB(c); if (c.IsSavable) c.Save(); } catch { }; } } public static void RefreshAyaNovaPartFromQB(Part c) { PopulateQBItemCache(); IntegrationMap im = QBOIntegration.Maps[c.ID]; if (im == null) return;//this part is not linked DataRow dr = _dtQBItems.Rows.Find(im.ForeignID); if (dr == null) return; //QBListID not found in part list? string sName = dr["FullName"].ToString(); if (sName.Length > 255) sName = sName.Substring(0, 255); c.PartNumber = sName; c.Name = T(255, dr["SalesDesc"].ToString()); c.WholesalerID = AyaVendorForQBItem(im.ForeignID); c.Retail = (decimal)dr["Price"]; c.Cost = (decimal)dr["Cost"]; } /// /// Import the indicated QB Item /// to an AyaNova Part record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportQBPart(string QuickBooksID, ArrayList alErrors) { DataRow dr = _dtQBItems.Rows.Find(QuickBooksID); //QBListID not found in Part list? if (dr == null) { alErrors.Add("ImportQBPart: ID not found " + QuickBooksID); return; } string sName = dr["FullName"].ToString(); if (sName.Length > 255) { alErrors.Add("ImportQBPart: QuickBooks Part name exceeds 255 character limit for AyaNova part number\r\n" + "Name: " + dr["FullName"].ToString() + "\r\n" + "was imported as: " + sName); sName = sName.Substring(0, 255); } try { //already a Part by that number? if (Part.Exists(Guid.Empty, dr["FullName"].ToString())) { alErrors.Add("ImportQBPart: Part number " + dr["FullName"].ToString() + " already exists in AyaNova"); return; } //Import seems safe... Part c = Part.NewItem(); c.PartNumber = sName; c.Name = T(255, dr["SalesDesc"].ToString()); //Set vendor ID based on prompted value c.WholesalerID = _ImportPartToAyaNovaDefaultVendorId; c.Retail = (decimal)dr["Price"]; c.Cost = (decimal)dr["Cost"]; if (!c.IsSavable) { alErrors.Add("ImportQBPart: AyaNova won't allow import of " + c.Name + "\r\n" + "Due to the following broken rules:\r\n" + c.GetBrokenRulesString()); return; } //Save and get ready to provide the ID c = (Part)c.Save(); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = sName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Part; m.LastSync = DateTime.Now; m.ForeignID = QuickBooksID; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportQBPart: AyaNova won't allow import / link of " + sName + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion #endregion Import to AyaNova #region Export / refresh to QuickBooks #region Customer #region Refresh to QB public static void RefreshQBClientFromAyaNova(List objectIDList) { foreach (Guid g in objectIDList) { try { Client c = Client.GetItemNoMRU(g); RefreshQBClientFromAyaNova(c); } catch { }; } } /// /// Refresh the indicated AyaNova client /// to it's linked QuickBooks customer record /// public static void RefreshQBClientFromAyaNova(Client c) { //////////////////////////////////////////////////////////// //NEW API TODO: //The DisplayName, Title, GivenName, MiddleName, FamilyName, Suffix, and PrintOnCheckName attributes must not contain colon (:), tab (\t), or newline (\n) characters. //https://developer.intuit.com/docs/api/accounting/customer IntegrationMap im = QBOIntegration.Maps[c.ID]; if (im == null) return;//this client is not linked DataRow dr = _dtQBClients.Rows.Find(im.ForeignID); if (dr == null) return; //QBListID not found in client list? try { string strSyncToken = GetQBCustomerSyncToken(im.ForeignID); if (string.IsNullOrEmpty(strSyncToken)) { MessageBox.Show("RefreshQBClientFromAyaNova -> Error: unable to fetch SyncToken from QuickBooks Online. No changes made."); return; } q.Customer ca = new q.Customer(); CopyAyClientToQBOCustomer(c, ca); ca.Id = im.ForeignID; ca.SyncToken = strSyncToken; //case 519 ca.SalesTermRef = new q.ReferenceType(); ca.SalesTermRef.Value = QDat.TermsDefault; //----------- UPDATE VIA API ------------- DataService service = new DataService(SC); q.Customer resultCustomer = service.Update(ca) as q.Customer; //after online record update, in turn update some local data: im.Name = resultCustomer.DisplayName; im.LastSync = DateTime.Now; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { MessageBox.Show("RefreshQBClientFromAyaNova: QuickBooks won't allow refresh of " + c.Name + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion Refresh to qb #region Copy customer from AY to QB /// /// Used for both update and create /// /// /// /// /// /// private static void CopyAyClientToQBOCustomer(Client c, q.Customer ca) { ca.sparse = true; ca.sparseSpecified = true; string sName = T(100, c.Name); //100 char limit in QBO //CLEAN THE NAME TO MEET QBO STANDARDS sName = sName.Replace(':', '-').Replace('\t', ' ').Replace('\n', ' '); ca.DisplayName = sName; ca.ContactName = c.Contact; ca.PrimaryPhone = new q.TelephoneNumber(); if (c.Phone1 != null) ca.PrimaryPhone.FreeFormNumber = T(21, c.Phone1); else ca.PrimaryPhone.FreeFormNumber = string.Empty; ca.Fax = new q.TelephoneNumber(); ca.Fax.FreeFormNumber = T(21, c.Phone2); ca.AlternatePhone = new q.TelephoneNumber(); ca.AlternatePhone.FreeFormNumber = T(21, c.Phone3); ca.PrimaryEmailAddr = new q.EmailAddress(); ca.PrimaryEmailAddr.Address = T(100, c.Email); #region Addresses bool bHasBillAddress = false; ca.BillAddr = new q.PhysicalAddress(); if (c.MailToAddress.DeliveryAddress != "") { bHasBillAddress = true; //QBO Max 2000 chars for all 5 line addresses combined but 500 per line of the 5 lines (? bizarre) string[] sad = T(2000, c.MailToAddress.DeliveryAddress).Replace("\r\n", "\n").Split('\n'); int sadLines = sad.GetLength(0); if (sadLines > 0) ca.BillAddr.Line1 = T(500, sad[0].TrimEnd()); if (sadLines > 1) ca.BillAddr.Line2 = T(500, sad[1].TrimEnd()); if (sadLines > 2) ca.BillAddr.Line3 = T(500, sad[2].TrimEnd()); if (sadLines > 3) ca.BillAddr.Line4 = T(500, sad[3].TrimEnd()); if (sadLines > 4) ca.BillAddr.Line5 = T(500, sad[4].TrimEnd()); } ca.BillAddr.City = T(255, c.MailToAddress.City); ca.BillAddr.Country = T(255, c.MailToAddress.Country); ca.BillAddr.PostalCode = T(31, c.MailToAddress.Postal); ca.BillAddr.CountrySubDivisionCode = T(255, c.MailToAddress.StateProv); bool bHasShipAddress = false; ca.ShipAddr = new q.PhysicalAddress(); if (c.GoToAddress.DeliveryAddress != "") { bHasShipAddress = true; //QBO Max 2000 chars for all 5 line addresses combined but 500 per line of the 5 lines (? bizarre) string[] sad = T(2000, c.GoToAddress.DeliveryAddress).Replace("\r\n", "\n").Split('\n'); int sadLines = sad.GetLength(0); if (sadLines > 0) ca.ShipAddr.Line1 = T(500, sad[0].TrimEnd()); if (sadLines > 1) ca.ShipAddr.Line2 = T(500, sad[1].TrimEnd()); if (sadLines > 2) ca.ShipAddr.Line3 = T(500, sad[2].TrimEnd()); if (sadLines > 3) ca.ShipAddr.Line4 = T(500, sad[3].TrimEnd()); if (sadLines > 4) ca.ShipAddr.Line5 = T(500, sad[4].TrimEnd()); } ca.ShipAddr.City = T(255, c.GoToAddress.City); ca.ShipAddr.Country = T(255, c.GoToAddress.Country); ca.ShipAddr.PostalCode = T(31, c.GoToAddress.Postal); ca.ShipAddr.CountrySubDivisionCode = T(255, c.GoToAddress.StateProv); #endregion //Added: 18-Nov-2006 CASE 95 //ensure that if only one address in ayanova that both types in QB get it if (!bHasShipAddress || !bHasBillAddress) { if (bHasShipAddress && !bHasBillAddress) { //copy shipping address to billing address CopyAddress(ca.ShipAddr, ca.BillAddr); } else if (bHasBillAddress && !bHasShipAddress) { CopyAddress(ca.BillAddr, ca.ShipAddr); } } ca.AcctNum = c.AccountNumber; } #endregion #region Export to QB //this code is called from AyaNova via plugin command menu on selected clients public static void ImportAyaClient(List objectIDList) { //case 3278 ask for taxes //WARNING: BEFORE MAKING CHANGES HERE SEE //MAP.CS line 1606 same code //AND QBOI.CS single client command code line 378 //Only USA uses one tax code for the whole customer //other regions are taxed by item and set with items if (Util.QUSA) { SetQBImportCustomerTaxCode s = new SetQBImportCustomerTaxCode(); if (s.ShowDialog() != DialogResult.OK) { s.Dispose(); return; } s.Dispose(); } ArrayList alErrors = new ArrayList(); foreach (Guid g in objectIDList) { try { ImportAyaClient(g, alErrors); } catch { }; } if (alErrors.Count != 0) { StringBuilder sb = new StringBuilder(); sb.Append("Export completed with some errors:\r\n\r\n"); foreach (object o in alErrors) { sb.Append((string)o); sb.Append("\r\n************\r\n"); } CopyableMessageBox cb = new CopyableMessageBox(sb.ToString()); cb.ShowDialog(); cb.Dispose(); } } /// /// Import the indicated client /// to QuickBooks customer record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportAyaClient(Guid ClientID, ArrayList alErrors) { //////////////////////////////////////////////////////////// //NEW API TODO: //The DisplayName, Title, GivenName, MiddleName, FamilyName, Suffix, and PrintOnCheckName attributes must not contain colon (:), tab (\t), or newline (\n) characters. if (!Client.Exists(ClientID, "")) { alErrors.Add("ImportAyaClient: Client not found in AyaNova (deleted recently?) (" + ClientID.ToString() + ")"); return; } Client c = Client.GetItem(ClientID); try { q.Customer ca = new q.Customer(); CopyAyClientToQBOCustomer(c, ca); if (ca.DisplayName != c.Name) { alErrors.Add("ImportAyaClient: AyaNova client name needed to be changed to meet QBOnline standards\r\n" + "Name: " + c.Name + "\r\n" + "will be imported as: " + ca.DisplayName); } //create a new row to add to the client cache datatable DataRow dr = _dtQBClients.NewRow(); dr["FullName"] = ca.DisplayName; dr["MailAddress"] = new Address(); dr["StreetAddress"] = new Address(); dr["Phone"] = ""; dr["Fax"] = ""; dr["AltPhone"] = ""; dr["Email"] = c.Email; dr["Contact"] = ""; dr["Created"] = DateTime.MinValue;//flag indicating fresh record incomplete dr["Modified"] = DateTime.MinValue;//ditto dr["Account"] = ""; //case 519 ca.SalesTermRef = new q.ReferenceType(); ca.SalesTermRef.Value = QDat.TermsDefault; //Only USA QB Online companies have a default tax code //non USA do it by item only and this field is not even present if (QUSA) { if (QDat.CustomerTaxCodeDefault != TAX_CODE_ID_NO_TAX) { ca.DefaultTaxCodeRef = new q.ReferenceType(); ca.DefaultTaxCodeRef.Value = QDat.CustomerTaxCodeDefault; ca.Taxable = true; ca.TaxableSpecified = true; } else { ca.Taxable = false; ca.TaxableSpecified = true; } } //============ ADD VIA API================================ DataService service = new DataService(SC); q.Customer resultCustomer = service.Add(ca) as q.Customer; //======================================================== //catch the new ID for the QB Item dr["ID"] = resultCustomer.Id; //add the new row for the newly imported object _dtQBClients.Rows.Add(dr); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = resultCustomer.DisplayName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Client; m.LastSync = DateTime.Now; m.ForeignID = resultCustomer.Id; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportAyaClient: QuickBooks won't allow import of " + c.Name + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion export to qb /// /// Copy the contents of one address to the other /// /// /// private static void CopyAddress(q.PhysicalAddress from, q.PhysicalAddress to) { to.City = from.City; to.Country = from.Country; to.CountryCode = from.CountryCode; to.CountrySubDivisionCode = from.CountrySubDivisionCode; to.Lat = from.Lat; to.Line1 = from.Line1; to.Line2 = from.Line2; to.Line3 = from.Line3; to.Line4 = from.Line4; to.Line5 = from.Line5; to.Long = from.Long; to.Note = from.Note; to.PostalCode = from.PostalCode; to.PostalCodeSuffix = from.PostalCodeSuffix; to.Tag = from.Tag; //CopyStringIfSet(from.City, to.City); //CopyStringIfSet(from.Country, to.Country); //CopyStringIfSet(from.CountryCode, to.CountryCode); //CopyStringIfSet(from.CountrySubDivisionCode, to.CountrySubDivisionCode); //CopyStringIfSet(from.Lat, to.Lat); //CopyStringIfSet(from.Line1, to.Line1); //CopyStringIfSet(from.Line2, to.Line2); //CopyStringIfSet(from.Line3, to.Line3); //CopyStringIfSet(from.Line4, to.Line4); //CopyStringIfSet(from.Line5, to.Line5); //CopyStringIfSet(from.Long, to.Long); //CopyStringIfSet(from.Note, to.Note); //CopyStringIfSet(from.PostalCode, to.PostalCode); //CopyStringIfSet(from.PostalCodeSuffix, to.PostalCodeSuffix); //CopyStringIfSet(from.Tag, to.Tag); } ///// ///// Convenience helper for copying address fields above ///// ///// ///// //private static void CopyStringIfSet(string from, string to) //{ // if (!string.IsNullOrWhiteSpace(from)) // to = from; //} #endregion customer #region Vendor #region Copy Vendor from AY to QB /// /// Used for both update and create /// /// /// /// /// /// private static void CopyAyVendorToQBOVendor(Vendor c, q.Vendor ca) { ca.sparse = true; ca.sparseSpecified = true; string sName = c.Name; //CLEAN THE NAME TO MEET QBO STANDARDS sName = sName.Replace(':', '-').Replace('\t', ' ').Replace('\n', ' '); ca.DisplayName = sName; ca.ContactName = c.Contact; ca.PrimaryPhone = new q.TelephoneNumber(); if (c.Phone1 != null) ca.PrimaryPhone.FreeFormNumber = c.Phone1; else ca.PrimaryPhone.FreeFormNumber = string.Empty; ca.Fax = new q.TelephoneNumber(); ca.Fax.FreeFormNumber = c.Phone2; ca.AlternatePhone = new q.TelephoneNumber(); ca.AlternatePhone.FreeFormNumber = c.Phone3; ca.PrimaryEmailAddr = new q.EmailAddress(); ca.PrimaryEmailAddr.Address = c.Email; #region Addresses ca.BillAddr = new q.PhysicalAddress(); if (c.MailToAddress.DeliveryAddress != "") { string[] sad = c.MailToAddress.DeliveryAddress.Replace("\r\n", "\n").Split('\n'); int sadLines = sad.GetLength(0); if (sadLines > 0) ca.BillAddr.Line1 = sad[0].TrimEnd(); if (sadLines > 1) ca.BillAddr.Line2 = sad[1].TrimEnd(); if (sadLines > 2) ca.BillAddr.Line3 = sad[2].TrimEnd(); if (sadLines > 3) ca.BillAddr.Line4 = sad[3].TrimEnd(); if (sadLines > 4) ca.BillAddr.Line5 = sad[4].TrimEnd(); } ca.BillAddr.City = c.MailToAddress.City; ca.BillAddr.Country = c.MailToAddress.Country; ca.BillAddr.PostalCode = c.MailToAddress.Postal; ca.BillAddr.CountrySubDivisionCode = c.MailToAddress.StateProv; //NO SHIP ADDRESS IN VENDOR FOR QBO (not in api docs) #endregion ca.AcctNum = c.AccountNumber; } #endregion /// /// Import the indicated Vendor /// to QuickBooks Vendor record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportAyaVendor(Guid VendorID, ArrayList alErrors) { if (!Vendor.Exists(VendorID, "")) { alErrors.Add("ImportAyaVendor: Vendor not found in AyaNova (deleted recently?) (" + VendorID.ToString() + ")"); return; } Vendor c = Vendor.GetItem(VendorID); try { q.Vendor ca = new q.Vendor(); CopyAyVendorToQBOVendor(c, ca); if (ca.DisplayName != c.Name) { alErrors.Add("ImportAyaVendor: AyaNova Vendor name needed to be changed to meet QBOnline standards\r\n" + "Name: " + c.Name + "\r\n" + "will be imported as: " + ca.DisplayName); } //create a new row to add to the client cache datatable DataRow dr = _dtQBVendors.NewRow(); dr["FullName"] = ca.DisplayName; dr["MailAddress"] = new Address(); dr["StreetAddress"] = new Address(); dr["Phone"] = ""; dr["Fax"] = ""; dr["AltPhone"] = ""; dr["Email"] = ""; dr["Contact"] = ""; dr["Created"] = DateTime.MinValue;//flag indicating fresh record incomplete dr["Modified"] = DateTime.MinValue;//ditto dr["Account"] = ""; //============ ADD VIA API================================ DataService service = new DataService(SC); q.Vendor resultVendor = service.Add(ca) as q.Vendor; //======================================================== //catch the new ID for the QB Item dr["ID"] = resultVendor.Id; //add the new row for the newly imported object _dtQBVendors.Rows.Add(dr); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = resultVendor.DisplayName; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Vendor; m.LastSync = DateTime.Now; m.ForeignID = resultVendor.Id; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportAyaVendor: QuickBooks won't allow import of " + c.Name + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion vendor #region Part case 632 #region Copy Part from AY to QB private static void CopyAyPartToQBOItem(Part c, q.Item ca) { ca.sparse = true; ca.sparseSpecified = true; //CLEAN THE NAME TO MEET QBO STANDARDS ca.Name = T(100, c.PartNumber.Replace(':', '-').Replace('\t', ' ').Replace('\n', ' ')); ca.Description = T(4000, PartPickList.GetOnePart(c.ID)[0].DisplayName(AyaBizUtils.GlobalSettings.DefaultPartDisplayFormat)); //Here we need to set the vendor ID //parseable from purchase desc. if (c.WholesalerID != Guid.Empty) { if (AyaVendorList.Contains(c.WholesalerID)) { ca.PurchaseDesc = "AyaNova Vendor:" + Util.AyaVendorList[c.WholesalerID].Name; } } ca.UnitPrice = c.Retail; ca.PurchaseCost = c.Cost; } #endregion /// /// Refresh the list of AyaNova parts to their linked QuickBooks item records /// /// public static void RefreshQBPartFromAyaNova(List objectIDList) { foreach (Guid g in objectIDList) { try { Part c = Part.GetItem(g); RefreshQBPartFromAyaNova(c); } catch { }; } } /// /// Refresh the indicated AyaNova part /// to it's linked QuickBooks item record /// public static void RefreshQBPartFromAyaNova(Part c) { IntegrationMap im = QBOIntegration.Maps[c.ID]; if (im == null) return;//this part is not linked DataRow dr = _dtQBItems.Rows.Find(im.ForeignID); if (dr == null) return; //QBListID not found in part list? try { string strSyncToken = GetInventoryItemSyncToken(im.ForeignID); if (string.IsNullOrEmpty(strSyncToken)) { MessageBox.Show("RefreshQBPartFromAyaNova -> Error: unable to fetch SyncToken from QuickBooks Online. No changes made."); return; } //----CREATE ITEM, SET API STUFF ------- q.Item ca = new q.Item(); ca.Id = im.ForeignID; ca.SyncToken = strSyncToken; CopyAyPartToQBOItem(c, ca); //----------- UPDATE VIA API ------------- DataService service = new DataService(SC); q.Item resultItem = service.Update(ca) as q.Item; //---------AFTER UPDATE---------- im.LastSync = DateTime.Now; im.Name = resultItem.Name; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { CopyableMessageBox cp = new CopyableMessageBox( "RefreshQBPartFromAyaNova: QuickBooks Online won't allow import of " + c.PartNumber + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); cp.ShowDialog(); } } #region AyaNova Part to Quickbooks /// /// Import a list of ayanova parts to QuickBooks /// /// public static void ImportAyaPart(List objectIDList, bool ImportAsTypeInventoryItem) { ArrayList alErrors = new ArrayList(); foreach (Guid g in objectIDList) { try { ImportAyaPart(g, alErrors, ImportAsTypeInventoryItem); } catch { }; } if (alErrors.Count != 0) { StringBuilder sb = new StringBuilder(); sb.Append("Export AyaNova parts to QuickBooks Online completed with some errors:\r\n\r\n"); foreach (object o in alErrors) { sb.Append((string)o); sb.Append("\r\n************\r\n"); } CopyableMessageBox cb = new CopyableMessageBox(sb.ToString()); cb.ShowDialog(); cb.Dispose(); } } /// /// Import the indicated part /// to QuickBooks Online item record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportAyaPart(Guid PartID, ArrayList alErrors, bool ImportAsTypeInventoryItem)//case 3296 { if (!Part.Exists(PartID, "")) { alErrors.Add("ImportAyaPart: Part not found in AyaNova (deleted recently?) (" + PartID.ToString() + ")"); return; } //Vet the tax code if non USA if (!QUSA) { if (string.IsNullOrWhiteSpace(_ImportPartToQBDefaultTaxCodeId)) { alErrors.Add("ImportAyaPart: There is no tax code selected, can't export to QB Online"); return; } } Part c = Part.GetItem(PartID); try { q.Item ca = new q.Item(); CopyAyPartToQBOItem(c, ca); //create a new row to add to the client cache datatable DataRow dr = _dtQBItems.NewRow(); dr["FullName"] = ca.Name; dr["Type"] = qbitemtype.Inventory; dr["Price"] = c.Retail; dr["Cost"] = c.Cost; dr["SalesDesc"] = ca.Description; dr["ReorderPoint"] = 0; dr["Modified"] = DateTime.MinValue; dr["VendorID"] = c.WholesalerID.ToString(); //------------------------ //Set the qb item values not set by copy earlier //since this is create we need to set some other stuff //REQUIRED ACCOUNTS //From the docs: https://developer.intuit.com/docs/api/accounting/item //IncomeAccountRef: required for Inventory and Service item types //ExpenseAccountRef: required for Inventory, NonInventory, and Service item types //AssetAccountRef: required for Inventory item types //Already know a rate imports with only income account ref set, //so despite docs assuming this is the case for a non-inventory part until otherwise ca.IncomeAccountRef = new q.ReferenceType(); ca.IncomeAccountRef.Value = QDat.QBInventoryIncomeAccountReference; //TAXES if (QUSA) { //Taxable ca.Taxable = Util.QDat.ImportItemTaxable; ca.TaxableSpecified = true; } else { ca.SalesTaxCodeRef = new q.ReferenceType(); ca.SalesTaxCodeRef.Value = _ImportPartToQBDefaultTaxCodeId; } //case 3296 if (ImportAsTypeInventoryItem) { ca.Type = q.ItemTypeEnum.Inventory; ca.TypeSpecified = true; //Inventory needs cogs and asset //set Cogs as ExpenseAccount not COGS account which is not present in online //even though it's surfaced in the API for desktop version ca.ExpenseAccountRef = new q.ReferenceType(); ca.ExpenseAccountRef.Value = QDat.QBInventoryCOGSAccountRef; ca.AssetAccountRef = new q.ReferenceType(); ca.AssetAccountRef.Value = QDat.QBInventoryAssetAccountRef; //in qbo you have to state if you want q on hand tracked or not when creating the item, cannot be reversed ca.TrackQtyOnHand = true; ca.TrackQtyOnHandSpecified = true; PartInventoryValuesFetcher pbw = PartInventoryValuesFetcher.GetItem(c.ID); ca.QtyOnHand = pbw.QuantityOnHand; ca.QtyOnHandSpecified = true; ca.InvStartDate = DateTime.Now; ca.InvStartDateSpecified = true; ca.ReorderPoint = pbw.MinStockLevel; ca.ReorderPointSpecified = true; ca.UnitPrice = c.Retail; ca.UnitPriceSpecified = true; ca.PurchaseCost = c.Cost; ca.PurchaseCostSpecified = true; } else { //QBO requires other type if not tracking inventory ca.Type = q.ItemTypeEnum.NonInventory; ca.TypeSpecified = true; ca.TrackQtyOnHand = false; ca.TrackQtyOnHandSpecified = true; ca.UnitPrice = c.Retail; ca.UnitPriceSpecified = true; } //============ ADD VIA API================================ DataService service = new DataService(SC); q.Item resultItem = service.Add(ca) as q.Item; /* * ValidationException was thrown.Details:Required parameter An inventory cost-of-goods-sold account is required if you are tracking inventory quantities for this product. is missing in the request * ValidationException was thrown.Details:Business Validation Error: When you create an item, if Track quantity on hand is turned off, the item cannot be of type Inventory. * ValidationException was thrown.Details:Value should be a valid date value:Supplied value: As of date for initial quantity on hand is required. */ //======================================================== //catch the new ID for the QB Item dr["ID"] = resultItem.Id; //add the new row for the newly imported object _dtQBItems.Rows.Add(dr); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = resultItem.Name; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Part; m.LastSync = DateTime.Now; m.ForeignID = resultItem.Id; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportAyaPart: QuickBooks Online won't allow import of " + c.PartNumber + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion AyaNova part to Quickbooks #endregion part #region Rate case 632 private static void CopyAyRateToQBOItem(RatePickList.RatePickListInfo c, q.Item ca) { ca.sparse = true; ca.sparseSpecified = true; //CLEAN THE NAME TO MEET QBO STANDARDS ca.Name = T(100, c.Name.Replace(':', '-').Replace('\t', ' ').Replace('\n', ' ')); ca.Description = T(4000, c.Description); ca.PurchaseDesc = ca.Description; ca.UnitPrice = c.Charge; ca.PurchaseCost = c.Cost; } /// /// Import the indicated service rate /// to QuickBooks item record /// /// /// An arraylist to hold strings indicating errors on fail public static void ImportAyaRate(Guid RateID, RatePickList ratelist, ArrayList alErrors) { if (!ratelist.Contains(RateID)) { alErrors.Add("ImportAyaRate: Rate not found in AyaNova (deleted recently?) (" + RateID.ToString() + ")"); return; } //Vet the tax code if non USA if (!QUSA) { if (string.IsNullOrWhiteSpace(_ImportRateToQBDefaultTaxCodeId)) { alErrors.Add("ImportAyaRate: There is no tax code selected, can't export to QB Online"); return; } } RatePickList.RatePickListInfo c = ratelist[RateID]; try { q.Item ca = new q.Item(); CopyAyRateToQBOItem(c, ca); //create a new row to add to the client cache datatable DataRow dr = _dtQBItems.NewRow(); dr["FullName"] = ca.Name; dr["Type"] = qbitemtype.Service; dr["Price"] = c.Charge; dr["Cost"] = c.Cost; dr["SalesDesc"] = ca.Description; dr["ReorderPoint"] = 0; dr["Modified"] = DateTime.MinValue; dr["VendorID"] = ""; //------------------------ //Set the qb item values ca.IncomeAccountRef = new q.ReferenceType(); ca.IncomeAccountRef.Value = QDat.QBServiceIncomeAccountRef; ca.Type = q.ItemTypeEnum.Service; ca.TypeSpecified = true; ca.UnitPrice = c.Charge; ca.UnitPriceSpecified = true; ca.PurchaseCost = c.Cost; ca.PurchaseCostSpecified = true; //TAXES if (QUSA) { //Taxable ca.Taxable = Util.QDat.ImportItemTaxable; ca.TaxableSpecified = true; } else { ca.SalesTaxCodeRef = new q.ReferenceType(); ca.SalesTaxCodeRef.Value = _ImportRateToQBDefaultTaxCodeId; } //============ ADD VIA API================================ DataService service = new DataService(SC); q.Item resultItem = service.Add(ca) as q.Item; //======================================================== //catch the new ID for the QB Item dr["ID"] = resultItem.Id; //add the new row for the newly imported object _dtQBItems.Rows.Add(dr); //Link IntegrationMap m = QBOIntegration.Maps.Add(QBOIntegration); m.Name = resultItem.Name; m.RootObjectID = c.ID; m.RootObjectType = RootObjectTypes.Rate; m.LastSync = DateTime.Now; m.ForeignID = resultItem.Id; QBOIntegration = (Integration)QBOIntegration.Save(); } catch (Exception ex) { alErrors.Add("ImportAyaRate: QuickBooks Online won't allow import of " + c.Name + "\r\n" + "Due to the following error:\r\n" + CrackException(ex)); } } #endregion rate private static string T(int nLength, string s) { if (s == null || s == "") return ""; if (s.Length <= nLength) return s; else return s.Substring(0, nLength); } #endregion export to quickbooks #endregion importexport #region Workorder mismatch scanning public enum MisMatchReason { NotLinkedToQB = 0, PriceDifferent = 1, NothingToInvoice = 2 } /// /// Mismatch properties /// A structure for storing mismatches identified ///so user can resolve them. /// public struct MisMatch { //public Guid WorkorderID; public Guid mRootObjectID; public RootObjectTypes mObjectType; public string mName; public MisMatchReason mReason; public decimal mAyaPrice; public decimal mQBPrice; public Guid mWorkorderItemPartID; public string mQBListID; public Guid RootObjectID { get { return mRootObjectID; } } public RootObjectTypes ObjectType { get { return mObjectType; } } public string Name { get { return mName; } } public MisMatchReason Reason { get { return mReason; } } public decimal AyaPrice { get { return mAyaPrice; } } public decimal QBPrice { get { return mQBPrice; } } public Guid WorkorderItemPartID { get { return mWorkorderItemPartID; } } public string QBListID { get { return mQBListID; } } } /// /// Given a workorder ID /// scans the objects in the workorder /// that need to be linked to QB for invoicing /// and on any error found adds them to the /// mismatched object array list /// /// Id of workorder being scanned /// An arraylist of mismatch objects /// An array of GUID values of workorderitemparts that have been set by /// user to forcibly use the price set on the workorderitem part even though /// it differs from the quickbooks price /// True if all links ok, false if there are any mismatches at all public static bool ScanLinksOK(Guid WorkorderID, ArrayList MisMatches, ArrayList PriceOverrides) { bool bReturn = true; bool bSomethingToInvoice = false; Workorder w = Workorder.GetItem(WorkorderID); //Client ok? if (!QBOIntegration.Maps.Contains(w.ClientID)) { bReturn = false; AddMisMatch(AyaClientList[w.ClientID].Name, w.ClientID, RootObjectTypes.Client, MisMatchReason.NotLinkedToQB, MisMatches); } //Service rates: foreach (WorkorderItem wi in w.WorkorderItems) { #region Labor foreach (WorkorderItemLabor wl in wi.Labors) { //If there's *any* labor then there is something to invoice bSomethingToInvoice = true; //Check that rate isn't actually guid.empty //it's possible that some users have not selected a rate on the workorder if (wl.ServiceRateID == Guid.Empty) throw new System.ApplicationException("ERROR: Workorder " + w.WorkorderService.ServiceNumber.ToString() + " has a labor item with no rate selected\r\n" + "This is a serious problem for QBOI and needs to be rectified before QBOI can be used.\r\n"); if (!QBOIntegration.Maps.Contains(wl.ServiceRateID)) { bReturn = false; AddMisMatch(AyaRateList[wl.ServiceRateID].Name, wl.ServiceRateID, RootObjectTypes.Rate, MisMatchReason.NotLinkedToQB, MisMatches); } } #endregion #region Travel foreach (WorkorderItemTravel wt in wi.Travels) { //If there's *any* travel then there is something to invoice bSomethingToInvoice = true; //Check that rate isn't actually guid.empty //it's possible that some users have not selected a rate on the workorder if (wt.TravelRateID == Guid.Empty) throw new System.ApplicationException("ERROR: Workorder " + w.WorkorderService.ServiceNumber.ToString() + " has a travel item with no rate selected\r\n" + "This is a serious problem for QBOI and needs to be rectified before QBOI can be used.\r\n"); if (!QBOIntegration.Maps.Contains(wt.TravelRateID)) { bReturn = false; AddMisMatch(AyaRateList[wt.TravelRateID].Name, wt.TravelRateID, RootObjectTypes.Rate, MisMatchReason.NotLinkedToQB, MisMatches); } } #endregion #region Parts foreach (WorkorderItemPart wp in wi.Parts) { //If there's *any* parts then there is something to invoice bSomethingToInvoice = true; //Changed: 14-Nov-2006 to check that linked item id exists in qb if (!QBOIntegration.Maps.Contains(wp.PartID) || QBItems.Rows.Find(QBOIntegration.Maps[wp.PartID].ForeignID) == null) { bReturn = false; //Changed: 21-June-2006 to use display formatted name AddMisMatch(AyaPartList[wp.PartID].DisplayName(Util.GlobalSettings.DefaultPartDisplayFormat), wp.PartID, RootObjectTypes.Part, MisMatchReason.NotLinkedToQB, MisMatches); } else { //check the price if (!PriceOverrides.Contains(wp.ID)) { decimal qbPrice = (decimal)QBItems.Rows.Find(QBOIntegration.Maps[wp.PartID].ForeignID)["Price"]; //------------DISCOUNT----------------- string disco = ""; //Added:20-July-2006 to incorporate discounts on parts into qb invoice decimal charge; //Changed: 18-Nov-2006 CASE 158 //this is all wrong, it was multiplying price by quantity to calculate charge when it shouldn't //removed quanty * price in next line to just price charge = decimal.Round(wp.Price, 2, MidpointRounding.AwayFromZero); charge = charge - (decimal.Round(charge * wp.Discount, 2, MidpointRounding.AwayFromZero)); if (wp.Discount != 0) { disco = " (Price " + wp.Price.ToString("c") + " discounted on workorder " + wp.Discount.ToString("p") + ") \r\n"; } //----------------------------- //It's a match, let's see if the price matches as well if (charge != qbPrice) { bReturn = false; AddMisMatch("WO: " + w.WorkorderService.ServiceNumber.ToString() + disco + " Part: " + AyaPartList[wp.PartID].DisplayName(Util.GlobalSettings.DefaultPartDisplayFormat),//Changed: 21-June-2006 to use display formatted name wp.PartID, RootObjectTypes.Part, MisMatchReason.PriceDifferent, MisMatches, qbPrice, charge, wp.ID, QBOIntegration.Maps[wp.PartID].ForeignID); } } } } #endregion #region Outside service charges if (wi.HasOutsideService) { if (wi.OutsideService.RepairPrice != 0 || wi.OutsideService.ShippingPrice != 0) { bSomethingToInvoice = true; //there is something billable, just need to make sure //that there is a QB charge defined for outside service if (QDat.OutsideServiceChargeAs == null || QDat.OutsideServiceChargeAs == "" || !QBItems.Rows.Contains(QDat.OutsideServiceChargeAs)) { bReturn = false; AddMisMatch("Outside service", Guid.Empty, RootObjectTypes.WorkorderItemOutsideService, MisMatchReason.NotLinkedToQB, MisMatches); } } } #endregion #region Workorder item loan charges if (wi.HasLoans) { foreach (WorkorderItemLoan wil in wi.Loans) { if (wil.Charges != 0) { //case 772 bSomethingToInvoice = true; //there is something billable, just need to make sure //that there is a QB charge defined for loaned item charges if (QDat.WorkorderItemLoanChargeAs == null || QDat.WorkorderItemLoanChargeAs == "" || !QBItems.Rows.Contains(QDat.WorkorderItemLoanChargeAs)) { bReturn = false; AddMisMatch("Workorder item loan", Guid.Empty, RootObjectTypes.WorkorderItemLoan, MisMatchReason.NotLinkedToQB, MisMatches); break; } } } } #endregion #region Workorder item misc expenses if (wi.HasExpenses) { foreach (WorkorderItemMiscExpense wie in wi.Expenses) { if (wie.ChargeToClient) { bSomethingToInvoice = true; //there is something billable, just need to make sure //that there is a QB charge defined for misc expense item charges if (QDat.MiscExpenseChargeAs == null || QDat.MiscExpenseChargeAs == "" || !QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) { bReturn = false; AddMisMatch("Workorder item expense", Guid.Empty, RootObjectTypes.WorkorderItemMiscExpense, MisMatchReason.NotLinkedToQB, MisMatches); break; } } } } #endregion }//workorder items loop //If there are no mismatches so far, //maybe it's because it's got nothing to invoice? if (bReturn && !bSomethingToInvoice) { bReturn = false; AddMisMatch("WO: " + w.WorkorderService.ServiceNumber.ToString() + " - Nothing chargeable on it, will not be invoiced", Guid.Empty, RootObjectTypes.Nothing, MisMatchReason.NothingToInvoice, MisMatches); } return bReturn; } private static void AddMisMatch(string Name, Guid RootObjectID, RootObjectTypes RootObjectType, MisMatchReason Reason, ArrayList Mismatches) { AddMisMatch(Name, RootObjectID, RootObjectType, Reason, Mismatches, 0m, 0m, Guid.Empty, ""); } private static void AddMisMatch(string Name, Guid RootObjectID, RootObjectTypes RootObjectType, MisMatchReason Reason, ArrayList Mismatches, decimal QBPrice, decimal AyaPrice, Guid WorkorderItemPartID, string QBListID) { bool bDuplicate = false; //scan through list of existing mismatches, //only add a not linked item if it's //not there already //other types of mismatches need to be added because //they need to be resolved on a case by case basis or are unresolvable if (Reason == MisMatchReason.NotLinkedToQB) { foreach (object o in Mismatches) { MisMatch m = (MisMatch)o; //Have to check ID and type here because for outside service //and loans and misc expenses the id is always empty so type is //the only way to differentiate if (m.RootObjectID == RootObjectID && m.ObjectType == RootObjectType) { bDuplicate = true; break; } } } if (!bDuplicate) { MisMatch m = new MisMatch(); m.mName = Name; m.mRootObjectID = RootObjectID; m.mObjectType = RootObjectType; m.mReason = Reason; m.mAyaPrice = AyaPrice; m.mQBPrice = QBPrice; m.mWorkorderItemPartID = WorkorderItemPartID; m.mQBListID = QBListID; Mismatches.Add(m); } } #endregion wo_mismatch_scan #region Invoice /// /// Invoice out the workorders contained in the array list /// as one single invoice /// /// Put in descriptive text as necessary and when sucessfully invoiced /// set workorders in list to status selected for post invoicing and /// set invoice number and then close them /// /// /// public static void Invoice(ArrayList alWorkorders, ArrayList alErrors) { //Example of how to make an invoice using .net api //https://gist.github.com/IntuitDeveloperRelations/6500373 if (alWorkorders.Count == 0) return; try { //Create invoice q.Invoice i = new q.Invoice(); //Private note? i.PrivateNote = "Imported from AyaNova by: " + Thread.CurrentPrincipal.Identity.Name + " @ " + DateTime.Now.ToString(); //set sales terms if (!string.IsNullOrWhiteSpace(QDat.TermsDefault)) { i.SalesTermRef = new q.ReferenceType(); i.SalesTermRef.Value = QDat.TermsDefault; } //a list of Line objects to hold the details of the invoice List AllInvoiceLines = new List(); //a string to hold the memo field StringBuilder sbMemo = new StringBuilder(); if (QDat.SetMemoField) { if (alWorkorders.Count > 1) { sbMemo.Append(" Workorders: "); } else { sbMemo.Append(" Workorder: "); } } //Keep track if first time through so //we can set invoice header stuff based on first workorder's client etc bool bFirstLoop = true; //Loop through alworkorders foreach (object o in alWorkorders) { Workorder w = Workorder.GetItem((Guid)o); if (bFirstLoop) { bFirstLoop = false; #region Set header info for invoice //Set client string QBCustomerId = QBOIntegration.Maps[w.ClientID].ForeignID; i.CustomerRef = new q.ReferenceType(); i.CustomerRef.Value = QBCustomerId; //Get cached client datarow DataRow drCustomerCache = QBClients.Rows.Find(QBCustomerId); //if USA set whole invoice taxes if (QUSA) { bool bTaxable = (bool)drCustomerCache["Taxable"]; string sTaxCode = drCustomerCache["TaxId"].ToString(); if (bTaxable && sTaxCode != TAX_CODE_ID_NO_TAX) { //Set a tax q.TxnTaxDetail txnTaxDetail = new q.TxnTaxDetail(); txnTaxDetail.TxnTaxCodeRef = new q.ReferenceType() { Value = sTaxCode }; i.TxnTaxDetail = txnTaxDetail; } } //Set email values //Line description if (QBClients.Rows.Contains(i.CustomerRef.Value)) { //Using QB description for row if (drCustomerCache != null && drCustomerCache["Email"] != null) { i.BillEmail = new q.EmailAddress(); i.BillEmail.Address = drCustomerCache["Email"].ToString(); //Set ToBeEmailed (if there is an address set) if (QDat.ToBeEmailed) { i.EmailStatusSpecified = true; i.EmailStatus = q.EmailStatusEnum.NeedToSend; } } } //Set QB Invoice template //NO TEMPLATES IN QB ONLINE SO FAR //Set Class (in docs it's settable for header (here) and for each line item. Header seems to do nothing but keeping this in just in case //and also setting it on each billable line item) //case 3267 if (QDat.TransactionClass != null && QDat.TransactionClass != TRANSACTION_CLASS_NO_CLASS_SELECTED) { i.ClassRef = new q.ReferenceType(); i.ClassRef.Value = QDat.TransactionClass; } //Set ToBePrinted if (QDat.ToBePrinted) { i.PrintStatus = q.PrintStatusEnum.NeedToPrint; i.PrintStatusSpecified = true; } #endregion set header info } else { //put a comma after the last workorder number that was //added to the memo string if (QDat.SetMemoField) sbMemo.Append(", "); } //if set memo true then build memo string from workorder service numbers etc if (QDat.SetMemoField) { sbMemo.Append(w.WorkorderService.ServiceNumber.ToString()); } #region Invoice header text if (QDat.HasInvoiceHeaderTemplate) { string s = QDat.InvoiceHeaderTemplate; if (s.IndexOf("~WO#~") != -1) { s = s.Replace("~WO#~", w.WorkorderService.ServiceNumber.ToString()); } if (s.IndexOf("~CONTACT~") != -1) { s = s.Replace("~CONTACT~", w.CustomerContactName); } if (s.IndexOf("~CREF#~") != -1) { s = s.Replace("~CREF#~", w.CustomerReferenceNumber); } if (s.IndexOf("~OURREF#~") != -1) { s = s.Replace("~OURREF#~", w.InternalReferenceNumber); } if (s.IndexOf("~PROJ~") != -1) { if (w.ProjectID == Guid.Empty) s = s.Replace("~PROJ~", ""); else s = s.Replace("~PROJ~", NameFetcher.GetItem("aProject", "aName", w.ProjectID).RecordName); } if (s.IndexOf("~CLIENT~") != -1) { s = s.Replace("~CLIENT~", NameFetcher.GetItem("aClient", "aName", w.ClientID).RecordName); } if (s.IndexOf("~SERVDATE~") != -1) { s = s.Replace("~SERVDATE~", w.WorkorderService.ServiceDate.ToString()); } if (s.IndexOf("~STAT~") != -1) { if (w.WorkorderService.WorkorderStatusID == Guid.Empty) s = s.Replace("~STAT~", ""); else s = s.Replace("~STAT~", NameFetcher.GetItem("aWorkorderStatus", "aName", w.WorkorderService.WorkorderStatusID).RecordName); } if (s.IndexOf("~DESC~") != -1) { s = s.Replace("~DESC~", w.Summary); } InvoiceAddText(AllInvoiceLines, s); } #endregion header text #region Part charges //case 3296 PartDisplayFormats defaultPartDisplayFormat = AyaBizUtils.GlobalSettings.DefaultPartDisplayFormat; foreach (WorkorderItem it in w.WorkorderItems) { foreach (WorkorderItemPart p in it.Parts) { //------------DISCOUNT----------------- //Added:20-July-2006 to incorporate discounts on parts into qb invoice decimal charge; //Case 269 this is incorrect: //charge = decimal.Round(p.Quantity * p.Price, 2, MidpointRounding.AwayFromZero); charge = decimal.Round(1 * p.Price, 2, MidpointRounding.AwayFromZero); charge = charge - (decimal.Round(charge * p.Discount, 2, MidpointRounding.AwayFromZero)); //----------------------------- //case 3296 - override description? string QBListID = QBOIntegration.Maps[p.PartID].ForeignID; DataRow drItemCacheData = _dtQBItems.Rows.Find(QBListID); qbitemtype qbType = (qbitemtype)drItemCacheData["type"]; string sOverrideDescription = ""; if (qbType == qbitemtype.NonInventory) { sOverrideDescription = PartPickList.GetOnePart(p.PartID)[0].DisplayName(defaultPartDisplayFormat); } InvoiceAddCharge(AllInvoiceLines, QBListID, p.Quantity, charge, sOverrideDescription);//case 3296 string sn = ""; if (p.PartSerialID != Guid.Empty) { sn = PartSerial.GetSerialNumberFromPartSerialID(p.PartSerialID); if (sn != "") InvoiceAddText(AllInvoiceLines, "SN: " + sn); } //Added:18-Nov-2006 case 125 //checks for nonempty description, also checks to see if description is //same as serial number because it's copied there in some cases and no sense //in showing it twice if (p.Description != "" && sn != p.Description) InvoiceAddText(AllInvoiceLines, p.Description); } } #endregion part charges #region Service charges foreach (WorkorderItem it in w.WorkorderItems) { foreach (WorkorderItemLabor l in it.Labors) { //Added 20-July-2006 to not charge for banked hours if (l.ServiceBankID != Guid.Empty) { InvoiceAddCharge(AllInvoiceLines, QBOIntegration.Maps[l.ServiceRateID].ForeignID, l.ServiceRateQuantity, 0); } else InvoiceAddCharge(AllInvoiceLines, QBOIntegration.Maps[l.ServiceRateID].ForeignID, l.ServiceRateQuantity, AyaRateList[l.ServiceRateID].Charge); } } #endregion Service charges #region Travel charges foreach (WorkorderItem it in w.WorkorderItems) { foreach (WorkorderItemTravel l in it.Travels) { InvoiceAddCharge(AllInvoiceLines, QBOIntegration.Maps[l.TravelRateID].ForeignID, l.TravelRateQuantity, AyaRateList[l.TravelRateID].Charge); } } #endregion Travel charges #region MiscExpense charges foreach (WorkorderItem it in w.WorkorderItems) { foreach (WorkorderItemMiscExpense l in it.Expenses) { if (l.ChargeToClient) { InvoiceAddCharge(AllInvoiceLines, QDat.MiscExpenseChargeAs, 1, l.ChargeAmount); } } } #endregion MiscExpense charges #region Loaner charges foreach (WorkorderItem it in w.WorkorderItems) { foreach (WorkorderItemLoan l in it.Loans) { InvoiceAddCharge(AllInvoiceLines, QDat.WorkorderItemLoanChargeAs, 1, l.Charges); } } #endregion Loaner charges #region OutsideService charges foreach (WorkorderItem it in w.WorkorderItems) { if (it.HasOutsideService && (it.OutsideService.ShippingPrice != 0 || it.OutsideService.RepairPrice != 0)) { InvoiceAddCharge(AllInvoiceLines, QDat.OutsideServiceChargeAs, 1, it.OutsideService.ShippingPrice + it.OutsideService.RepairPrice); } } #endregion OutsideService charges #region Descriptive footer text //Loop through workorder items //inserting descriptive text as required if (QDat.HasAnyInvoiceFooterTemplateFields) { foreach (WorkorderItem it in w.WorkorderItems) { #region Item (footer) fields if (QDat.InvoiceFooterTemplate != "") { string s = QDat.InvoiceFooterTemplate; if (s.IndexOf("~ITEM_SUMMARY~") != -1) { s = s.Replace("~ITEM_SUMMARY~", it.Summary); } if (s.IndexOf("~ITEM_SERVICE_NOTES~") != -1) { s = s.Replace("~ITEM_SERVICE_NOTES~", it.TechNotes); } if (s.IndexOf("~ITEM_TYPE~") != -1) { if (it.TypeID == Guid.Empty) s = s.Replace("~ITEM_TYPE~", ""); else s = s.Replace("~ITEM_TYPE~", NameFetcher.GetItem("aWorkorderItemType", "aName", it.TypeID).RecordName); } if (s.IndexOf("~ITEM_REQUEST_DATE~") != -1) { s = s.Replace("~ITEM_REQUEST_DATE~", it.RequestDate.ToString()); } if (s.IndexOf("~ITEM_STATUS~") != -1) { if (it.WorkorderStatusID == Guid.Empty) s = s.Replace("~ITEM_STATUS~", ""); else s = s.Replace("~ITEM_STATUS~", NameFetcher.GetItem("aWorkorderStatus", "aName", it.WorkorderStatusID).RecordName); } InvoiceAddText(AllInvoiceLines, s); } #endregion item #region Unit fields if (QDat.InvoiceUnitTemplate != "" && it.UnitID != Guid.Empty) { string s = QDat.InvoiceUnitTemplate; UnitPickList up = UnitPickList.GetListOfOneSpecificUnit(it.UnitID); if (s.IndexOf("~AYAFORMAT~") != -1) { s = s.Replace("~AYAFORMAT~", up[0].UnitName()); } if (s.IndexOf("~UNIT_SN~") != -1) { s = s.Replace("~UNIT_SN~", up[0].Serial); } if (s.IndexOf("~UNIT_METER~") != -1) { if (!up[0].Metered) s = s.Replace("~UNIT_METER~", ""); else s = s.Replace("~UNIT_METER~", Unit.LastMeterReading(up[0].ID).ToString()); } if (s.IndexOf("~UNIT_MAKE~") != -1) { s = s.Replace("~UNIT_MAKE~", up[0].VendorName); } if (s.IndexOf("~UNIT_MODEL_NAME~") != -1) { s = s.Replace("~UNIT_MODEL_NAME~", up[0].ModelName); } if (s.IndexOf("~UNIT_MODEL_NUMBER~") != -1) { s = s.Replace("~UNIT_MODEL_NUMBER~", up[0].ModelNumber); } InvoiceAddText(AllInvoiceLines, s); } #endregion unit #region Labor fields if (QDat.InvoiceServiceTemplate != "" && it.HasLabor) { foreach (WorkorderItemLabor wl in it.Labors) { string s = QDat.InvoiceServiceTemplate; if (s.IndexOf("~SERVICE_START~") != -1) { s = s.Replace("~SERVICE_START~", wl.ServiceStartDate.ToString()); } if (s.IndexOf("~SERVICE_STOP~") != -1) { s = s.Replace("~SERVICE_STOP~", wl.ServiceStopDate.ToString()); } if (s.IndexOf("~SERVICE_QUANTITY~") != -1) { s = s.Replace("~SERVICE_QUANTITY~", wl.ServiceRateQuantity.ToString()); } if (s.IndexOf("~NO_CHARGE_QUANTITY~") != -1) { s = s.Replace("~NO_CHARGE_QUANTITY~", wl.NoChargeQuantity.ToString()); } if (s.IndexOf("~RATE_NAME~") != -1) { s = s.Replace("~RATE_NAME~", AyaRateList[wl.ServiceRateID].Name); } if (s.IndexOf("~SERVICE_TECH~") != -1) { s = s.Replace("~SERVICE_TECH~", UserPickList.GetListOfOneSpecificUser(wl.UserID)[0].Name); } if (s.IndexOf("~DETAILS~") != -1) { s = s.Replace("~DETAILS~", wl.ServiceDetails); } InvoiceAddText(AllInvoiceLines, s); } } #endregion service #region Travel fields if (QDat.InvoiceTravelTemplate != "" && it.HasTravel) { foreach (WorkorderItemTravel wt in it.Travels) { string s = QDat.InvoiceTravelTemplate; if (s.IndexOf("~TRAVEL_START~") != -1) { s = s.Replace("~TRAVEL_START~", wt.TravelStartDate.ToString()); } if (s.IndexOf("~TRAVEL_STOP~") != -1) { s = s.Replace("~TRAVEL_STOP~", wt.TravelStopDate.ToString()); } if (s.IndexOf("~TRAVEL_QUANTITY~") != -1) { s = s.Replace("~TRAVEL_QUANTITY~", wt.TravelRateQuantity.ToString()); } if (s.IndexOf("~TRAVEL_NO_CHARGE_QUANTITY~") != -1) { s = s.Replace("~TRAVEL_NO_CHARGE_QUANTITY~", wt.NoChargeQuantity.ToString()); } if (s.IndexOf("~TRAVEL_RATE_NAME~") != -1) { s = s.Replace("~TRAVEL_RATE_NAME~", AyaRateList[wt.TravelRateID].Name); } if (s.IndexOf("~TRAVEL_TECH~") != -1) { s = s.Replace("~TRAVEL_TECH~", UserPickList.GetListOfOneSpecificUser(wt.UserID)[0].Name); } if (s.IndexOf("~TRAVEL_DETAILS~") != -1) { s = s.Replace("~TRAVEL_DETAILS~", wt.TravelDetails); } if (s.IndexOf("~TRAVEL_DISTANCE~") != -1) { s = s.Replace("~TRAVEL_DISTANCE~", wt.Distance.ToString()); } InvoiceAddText(AllInvoiceLines, s); } } #endregion travel fields #region Outside fields if (QDat.OutsideServiceChargeAs != "" && it.HasOutsideService) { string s = QDat.InvoiceOutsideServiceTemplate; if (s.IndexOf("~REPAIR_PRICE~") != -1) { s = s.Replace("~REPAIR_PRICE~", it.OutsideService.RepairPrice.ToString("c")); } if (s.IndexOf("~SHIP_CHARGE~") != -1) { s = s.Replace("~SHIP_CHARGE~", it.OutsideService.ShippingPrice.ToString("c")); } if (s.IndexOf("~SENT~") != -1) { s = s.Replace("~SENT~", it.OutsideService.DateSent.ToString()); } if (s.IndexOf("~RETURNED~") != -1) { s = s.Replace("~RETURNED~", it.OutsideService.DateReturned.ToString()); } if (s.IndexOf("~NOTES~") != -1) { s = s.Replace("~NOTES~", it.OutsideService.Notes); } InvoiceAddText(AllInvoiceLines, s); } #endregion outside service #region Misc expense fields if (QDat.InvoiceMiscExpenseTemplate != "" && it.HasExpenses) { foreach (WorkorderItemMiscExpense e in it.Expenses) { string s = QDat.InvoiceMiscExpenseTemplate; if (s.IndexOf("~CHARGES~") != -1) { s = s.Replace("~CHARGES~", e.ChargeAmount.ToString("c")); } if (s.IndexOf("~SUMMARY~") != -1) { s = s.Replace("~SUMMARY~", e.Name); } if (s.IndexOf("~DESCRIPTION~") != -1) { s = s.Replace("~DESCRIPTION~", e.Description); } if (s.IndexOf("~TECH~") != -1) { s = s.Replace("~TECH~", UserPickList.GetListOfOneSpecificUser(e.UserID)[0].Name); } InvoiceAddText(AllInvoiceLines, s); } } #endregion misc expense #region Loan item fields if (QDat.InvoiceLoanItemTemplate != "" && it.HasLoans) { foreach (WorkorderItemLoan l in it.Loans) { string s = QDat.InvoiceLoanItemTemplate; if (s.IndexOf("~CHARGE~") != -1) { s = s.Replace("~CHARGE~", l.Charges.ToString("c")); } if (s.IndexOf("~ITEM~") != -1) { s = s.Replace("~ITEM~", NameFetcher.GetItem("aLoanItem", "aName", l.LoanItemID).RecordName); } if (s.IndexOf("~LOANED~") != -1) { s = s.Replace("~LOANED~", l.OutDate.ToString()); } if (s.IndexOf("~LOAN_RETURNED~") != -1) { s = s.Replace("~LOAN_RETURNED~", l.ReturnDate.ToString()); } if (s.IndexOf("~LOAN_NOTES~") != -1) { s = s.Replace("~LOAN_NOTES~", l.Notes); } InvoiceAddText(AllInvoiceLines, s); } } #endregion loan item expense } } #endregion footer text }//Bottom of foreach workorder loop //Set all invoice lines i.Line = AllInvoiceLines.ToArray(); //Set memo field if (QDat.SetMemoField) { i.CustomerMemo = new q.MemoRef(); i.CustomerMemo.Value = T(1000, sbMemo.ToString()); } //============ ADD VIA API================================ DataService service = new DataService(SC); q.Invoice resultInvoice = service.Add(i) as q.Invoice; //======================================================== /* */ //--------- UPDATE OBJECTS FROM RESULT OF ADDING INVOICE --------------- string InvoiceNumber = resultInvoice.DocNumber; //Loop through all workorders again and set their invoice number, status and close them foreach (object o in alWorkorders) { Workorder w = Workorder.GetItem((Guid)o); if (QDat.PostWOStatus != Guid.Empty) { w.WorkorderService.WorkorderStatusID = QDat.PostWOStatus; } w.WorkorderService.InvoiceNumber = InvoiceNumber; //Case 7 if (QDat.AutoClose) w.Closed = true; w.Save(); } }//end try block catch (Exception ex) { alErrors.Add("Invoice: Invoicing failed due to the following error:\r\n" + CrackException(ex)); } } /// /// Add text to the invoice /// chopping into chunks less than the max 4095 characters limit /// as necessary /// private static void InvoiceAddText(List lineList, string Text) { if (Text == null || Text == "") return; List textLines = new List(); //break into 4000 character chunks as that's the limit for a detail line descripttion while (Text.Length > 4000) { textLines.Add(Text.Substring(0, 4000)); Text = Text.Remove(0, 4000); } //Get the last bit if (Text.Length > 0) textLines.Add(Text); //Loop through lines and add them one at a time to the list foreach (string s in textLines) { q.Line line = new q.Line(); line.Description = s; line.DetailType = q.LineDetailTypeEnum.DescriptionOnly; line.DetailTypeSpecified = true; lineList.Add(line); } } /// /// Add charge line to the invoice /// /// private static void InvoiceAddCharge(List lineList, string QBListID, decimal Quantity, decimal rate, string OverrideDescriptionText = "") { q.Line line = new q.Line(); line.Amount = rate * Quantity; line.AmountSpecified = true; //Line Sales Item Line Detail - UnitPrice //https://gist.github.com/IntuitDeveloperRelations/6500373 //Get item from cache DataRow drItemCacheData = _dtQBItems.Rows.Find(QBListID); //case 3296 //Line item DESCRIPTION if (string.IsNullOrWhiteSpace(OverrideDescriptionText)) { line.Description = drItemCacheData["SalesDesc"].ToString(); } else { line.Description = OverrideDescriptionText; } //TAXES string sItemTaxStatus = string.Empty; //US is different than all others looked at if (QCountry == "US") { sItemTaxStatus = "NON"; if ((bool)drItemCacheData["Taxable"]) { sItemTaxStatus = "TAX"; } } else { //NON-US means we should take the tax code specified on the QB Item and apply it here sItemTaxStatus = drItemCacheData["TaxId"].ToString(); } line.DetailType = q.LineDetailTypeEnum.SalesItemLineDetail; line.DetailTypeSpecified = true; q.ReferenceType transClass = null; if (QDat.TransactionClass != null && QDat.TransactionClass != TRANSACTION_CLASS_NO_CLASS_SELECTED) { transClass = new q.ReferenceType(); transClass.Value = QDat.TransactionClass; } line.AnyIntuitObject = new q.SalesItemLineDetail() { Qty = Quantity, QtySpecified = true, ItemRef = new q.ReferenceType() { Value = QBListID }, AnyIntuitObject = rate, ItemElementName = q.ItemChoiceType.UnitPrice, TaxCodeRef = new q.ReferenceType() { Value = sItemTaxStatus }, ClassRef = transClass }; lineList.Add(line); //QBO MAY BE RELEVANT: For Invoice objects in global locales: when updating Amount, remove the TxnTaxDetail element in the object before submitting it in the update request payload. } #endregion #region Change QB Item price public static void ChangeQBItemPrice(string QBListID, decimal NewPrice) { //TODO: QBO likely doesn't have the issues that caused case 92 in the first place, but //not sure if it's an issue either way. //Added: 18-Nov-2006 CASE 92 //check if inventory item as this code only handles price changing for inventory //items in qb qbitemtype qtype = (qbitemtype)QBItems.Rows.Find(QBListID)["Type"]; if (qtype != qbitemtype.Inventory) { MessageBox.Show("Only inventory items in QuickBooks can have their price changed\r\n" + "The current item is of type: " + qtype.ToString()); return; } try { //----Get the SyncToken----- string strSyncToken = GetInventoryItemSyncToken(QBListID); if (string.IsNullOrEmpty(strSyncToken)) { MessageBox.Show("ChangeQBItemPrice -> Error: unable to fetch SyncToken from QuickBooks Online. No changes made."); return; } //----CREATE ITEM, SET API STUFF ------- q.Item ca = new q.Item(); ca.sparse = true; ca.sparseSpecified = true; ca.Id = QBListID; ca.SyncToken = strSyncToken; //------- SET DATA ----------------- ca.UnitPrice = NewPrice; //----------- UPDATE VIA API ------------- DataService service = new DataService(SC); q.Item resultItem = service.Update(ca) as q.Item; //---------AFTER UPDATE---------- QBItems.Rows.Find(QBListID)["Price"] = NewPrice; } catch (Exception ex) { IntegrationLog.Log(QBID, "ChangeQBItemPrice: Failed with exception:" + CrackException(ex)); throw; } } public static string GetInventoryItemSyncToken(string QBListID) { try { QueryService qs = new QueryService(SC); List items = qs.ExecuteIdsQuery("select synctoken from Item where id = '" + QBListID + "'").ToList(); if (items.Count < 1) return string.Empty; else return items[0].SyncToken; } catch (Exception ex) { IntegrationLog.Log(QBID, "GetInventoryItemSyncToken: Failed with exception:" + CrackException(ex)); throw; } } #endregion public static string CrackException(Exception ex) { StringBuilder sb = new StringBuilder(); //More graceful error if it's an business rule exception if (ex is Intuit.Ipp.Exception.IdsException && ex.InnerException != null) { if (ex.InnerException is Intuit.Ipp.Exception.ValidationException) { sb.AppendLine("QuickBooks Online validation rule broken:"); if (ex.InnerException.Message.Contains("Make sure all your transactions have a GST/HST rate before you save.")) { sb.AppendLine("-----------------------------------"); sb.AppendLine("Most likely reason for this error:"); sb.AppendLine("You are attempting to Invoice one or more items that do not have a sales tax set in QuickBooks Online."); sb.AppendLine("-----------------------------------"); } } else sb.AppendLine("QuickBooks Online returned this error message:"); sb.AppendLine(ex.InnerException.Message.Replace("ValidationException was thrown.Details:", "")); } else { sb.AppendLine(ex.Message); sb.AppendLine("-------TRACE------"); sb.AppendLine(ex.StackTrace); while (ex.InnerException != null) { ex = ex.InnerException; sb.AppendLine("--- INNER EXCEPTION ---"); sb.AppendLine(ex.Message); sb.AppendLine("-------TRACE------"); sb.AppendLine(ex.StackTrace); } } return sb.ToString(); } /// /// Case 262 addition /// Convert a string to a double handling french canadian locale /// , since qb is not locale aware in it's api /// conversion can fail because .net expects a comma that isn't there /// /// case 262 redux, changed to use tryparse and explicitly replace comma if present /// for french canadian versions /// /// /// /// public static double SafeToDouble(string s) { if (string.IsNullOrEmpty(s)) return 0; if (s.Contains(",")) s = s.Replace(",", "."); double retvalue = 0; if (!double.TryParse(s, out retvalue)) { try { retvalue = Convert.ToDouble(s, System.Globalization.CultureInfo.InvariantCulture); } catch (System.FormatException) { CopyableMessageBox cp = new CopyableMessageBox("SafeToDouble: Can't parse QB string double version number:\r\n[" + s + "]\r\nPlease copy and send this message to AyaNova tech support at support@ayanova.com"); cp.ShowDialog(); throw new System.ApplicationException("SafeToDouble: Can't parse QB string double value number: \"" + s + "\""); } } return retvalue; } //case 632 (sort of) public static void SaveQBIData() { if (QDat.IsDirty) { QBOIntegration.AIObject = QDat.XMLData; QBOIntegration = (Integration)QBOIntegration.Save(); QDat.IsDirty = false; } } //-------------------------------------------------------------------------------- } }