Files
ayanova7/source/Plugins/AyaNova.Plugin.QBOI/Util.cs
2019-10-14 18:35:18 +00:00

5630 lines
212 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
//oauth2
using Intuit.Ipp.OAuth2PlatformClient;
using Newtonsoft.Json.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>
/// Summary description for Util.
/// </summary>
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<string, string> QCompOtherPrefs = new Dictionary<string, string>();
//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;
/// <summary>
/// The official and unchanging integration ID for
/// QBOI
/// </summary>
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()
{
}
/// <summary>
/// Are you sure prompt
/// </summary>
/// <param name="msg"></param>
/// <param name="title"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Gets icon image from assembly
/// </summary>
/// <param name="ImageName"></param>
/// <returns></returns>
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));
}
/// <summary>
/// Invert a color so it's readable against the passed
/// in color
/// </summary>
/// <param name="col"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Checks to see if an item is linked already
/// </summary>
/// <param name="AyaNovaID"></param>
/// <returns></returns>
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";
//oAuth2 new variables
public static Intuit.Ipp.OAuth2PlatformClient.OAuth2Client oac = null;
//SANDBOX
//const string Q2_CLIENT_ID = "ABj70Wv5gDauFd9KgKFwuvpQjfzTwEgodEG8tnBbS8mSQhNrZJ";
//const string Q2_CLIENT_SECRET = "XUmJyvEcEuwQuyhARUAm0a8G3gzbEAeMiATCLyFZ";
//const string Q2_ENVIRONMENT = "Sandbox";
//const string Q2_QBO_BASE_URL = "https://sandbox-quickbooks.api.intuit.com/";
//PRODUCTION
//const string Q2_CLIENT_ID = "ABdUcQuZU9RJuVta7Jqbul85LnTpBwuR54hmvgAMn7NnZOuZam";
//const string Q2_CLIENT_SECRET = "iKd71iFV4PTaSlm22jB084RuiDIXcFfIEaOp88n4";
//xor encrypted
const string Q2_CLIENT_ID = "\b,:\n2;O=#3=A%%VC#\a7\v=\\W\r$.$\rR\a\0, 9$";
const string Q2_CLIENT_SECRET = " %XX\n#Z&;\b0\t$\\D+S]}<-*=*(&,*9VN]";
const string Q2_ENVIRONMENT = "Production";
const string Q2_QBO_BASE_URL = "https://quickbooks.api.intuit.com/";
const string Q2_REDIRECT_URL = "https://qboauth.ayanova.com/redirect";
const double Q2_TOKEN_LIFESPAN_MINUTES = 45;//Minutes to hold on to access token before refreshing. Right now oct 2019 it's one hour lifespan.
static string QBOI2_SESSION_TOKEN = "";
public static string ACCESS_TOKEN { get; set; }
public static string REFRESH_TOKEN { get; set; }
public static string REALM_ID { get; set; }
public static DateTime TOKEN_BIRTHDAY { get; set; }
//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();
}
//OAUTH 1.x code, unchanged.
// static ServiceContext SC = null;
// static DesktopIppOAuth.OAuthConnector CN;
// //case 3671
// //Modify the below code to instead work with a static tokens obtained from qBridge (just for initial testing)
////then test that all ops work witha fresh company connection
// static public void StartAuthorization()
// {
// //case 3669
// //https://help.developer.intuit.com/s/question/0D50f00005gFeqpCAC/how-do-i-suddenly-get-there-was-an-error-while-communicating-with-the-ids-server
// System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; //Add this just to be sure to use TLS1.2
// //#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");
// }
//Note, always returns a positive number, it's plenty random
public static Int64 RandomInt64()
{
using (System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
{
var buffer = new byte[sizeof(Int64)];
rng.GetBytes(buffer);
return Math.Abs(BitConverter.ToInt64(buffer, 0));
}
}
public static string RandomSessionToken()
{
using (System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
{
var buffer = new byte[24];
rng.GetBytes(buffer);
return Convert.ToBase64String(buffer).TrimEnd('=');
}
}
/// <summary>
/// Refresh token if necessary, call this regularly
/// will check if access token is approaching expiry (1 hour)
/// and will refresh as necessary
/// </summary>
public static void RefreshTokens()
{
TimeSpan age = DateTime.Now.Subtract(TOKEN_BIRTHDAY);
if (age.TotalMinutes < Q2_TOKEN_LIFESPAN_MINUTES)
{
return;
}
IntegrationLog.Log(QBID, "RefreshTokens: Token expired, fetching new one...");
try
{
//refresh token here
var v = oac.RefreshTokenAsync(REFRESH_TOKEN).Result;
TOKEN_BIRTHDAY = DateTime.Now;
ACCESS_TOKEN = v.AccessToken;
REFRESH_TOKEN = v.RefreshToken;
}
catch (Exception ex)
{
IntegrationLog.Log(QBID, "RefreshTokens: Exception error attempting to refresh token:");
IntegrationLog.Log(QBID, CrackException(ex));
}
}
static ServiceContext SC = null;
//static DesktopIppOAuth.OAuthConnector CN;
//case 3671
//Modify the below code to instead work with a static tokens obtained from qBridge (just for initial testing)
//then test that all ops work witha fresh company connection
static public bool StartAuthorization()
{
//used to tie this session to the auth key on our server for fetching later
QBOI2_SESSION_TOKEN = RandomInt64().ToString();
//shell out to browser for user to login to QB
System.Diagnostics.Process.Start("https://qboauth.ayanova.com/start/" + QBOI2_SESSION_TOKEN + "/"+ AyaBizUtils.LicId);
//Open dialog that polls for token repeatedly, there is a delay built into the server
//so no need to delay here, just keep attempting to fetch over and over or until cancel
GetToken gt = new GetToken(QBOI2_SESSION_TOKEN);
DialogResult tokenResult = gt.ShowDialog();
if (tokenResult == DialogResult.Cancel)
{
if (!string.IsNullOrEmpty(gt.LastError))
{
IntegrationLog.Log(QBID, "PFC: QuickBooks online failed to retrieve token");
IntegrationLog.Log(QBID, "PFC: token fetch error: " + gt.LastError);
}
return false;
}
if (string.IsNullOrEmpty(gt.FetchedToken))
{
IntegrationLog.Log(QBID, "PFC: QuickBooks online retrieved token is empty! Unable to proceed.");
return false;
}
//parse the token and store the values in the session variables
JObject j = JObject.Parse(gt.FetchedToken);
/* example return value
{
"realmId": "193514527954149",
"access_token": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..f_bBkYjAwaN2H9cVLHDDog.XNz0F7PN96vJhpdn6y-yVv85sqBGI0QQYucLQ4NjzKfk3eKmTh0es8hJofZufequZu1dqy1q8wIuJ3kGABsKqL6yUy3kDg9jrED-bgqMaDHAbafLdaMuzVmzwF2pnR1M61ey5rxfio2Nr54q9_HhRJpealHyiM4hDXhA87FVbMP1pqH43HB5dt6WIu4EFnNX-HSWEZCa-LeYTn39bGpCdxA89WQyi4B_fnVvF1YhbzWe3GINZV3bHrVlD9flslBuym_HHNISapSK9AKwx71upAgRhxGf9Q5aBvb3w5-HCQYTbZZv2oooTApFP6SnrxhEC7d11h2e2_cDlCvvxeGaK5ZqmeOzwZKY72Fmum50UB61ChoLEFl_odW2e8GZY_f7EavufVklj9yWu-zuFeExSwHEP-QCbFZ10EvP5BIxB0os2vRXfS906zVpFJACuAaoVrfpIlRa0QIPMpwLuN50lONPMV1xBRGuodqs7R550Pm-5Bff6-Im-ialg9sLkETSsE9ICsJ7z4p2wQ1Z9b_Cdz6fbOUSotv8uwK2ke4mKTRKejbl6UfDG8DNoeCGMcaF7VDLd_8X8UdMU2gYH6Y9EEOFbX6MOpmUUIsyd-0WLZjDTya6bbLONsrpZmZXXX12n5pDykXxachl6I6XR0vuhGnF6Z_CSvOjYWrArQ8IY8SF9oobgFWi0wy_izurVq5R.ltpcEsh0zXmOBmRTggommQ",
"refresh_token": "AB11578778808Neoah1qLV0tgnNrnr4T9mJkIlTY5nZdOieQdo",
"tokenBirthday": "2019-10-02T14:40:08.4855781-07:00"
}
* */
ACCESS_TOKEN = j["access_token"].Value<string>();
REFRESH_TOKEN = j["refresh_token"].Value<string>();
REALM_ID = j["realmId"].Value<string>();
//start the clock on the tokens
TOKEN_BIRTHDAY = DateTime.Now;
oac = new OAuth2Client(Ec(Q2_CLIENT_ID, "Invoice"), Ec(Q2_CLIENT_SECRET, "Invoice"), Q2_REDIRECT_URL, Q2_ENVIRONMENT);
//case 3669
//https://help.developer.intuit.com/s/question/0D50f00005gFeqpCAC/how-do-i-suddenly-get-there-was-an-error-while-communicating-with-the-ids-server
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; //Add this just to be sure to use TLS1.2
Intuit.Ipp.Security.OAuth2RequestValidator validator = new Intuit.Ipp.Security.OAuth2RequestValidator(ACCESS_TOKEN);
SC = new Intuit.Ipp.Core.ServiceContext(REALM_ID, Intuit.Ipp.Core.IntuitServicesType.QBO, validator);
//SC.IppConfiguration.Logger.RequestLog.EnableRequestResponseLogging = true;
// SC.IppConfiguration.Logger.RequestLog.ServiceRequestLoggingLocation = "c:\\temp\\qblogs\\";
//' Common setting for both ServiceContexts:
SC.IppConfiguration.MinorVersion.Qbo = "23";
SC.IppConfiguration.BaseUrl.Qbo = Q2_QBO_BASE_URL;
//' Serialization Format Json or xml
SC.IppConfiguration.Message.Request.SerializationFormat = Intuit.Ipp.Core.Configuration.SerializationFormat.Json;
SC.IppConfiguration.Message.Response.SerializationFormat = Intuit.Ipp.Core.Configuration.SerializationFormat.Json;
//' Compression Format can be GZip or Deflate:
SC.IppConfiguration.Message.Request.CompressionFormat = Intuit.Ipp.Core.Configuration.CompressionFormat.GZip;
SC.IppConfiguration.Message.Response.CompressionFormat = Intuit.Ipp.Core.Configuration.CompressionFormat.GZip;
//' Retry 5 times at an interval of 1 Second if service fails. Note that RetryPolicy moved from Intuit.Ipp.Retry to Intuit.Ipp.Core.
TimeSpan retryInterval = new TimeSpan(0, 0, 0, 1);
SC.IppConfiguration.RetryPolicy = new Intuit.Ipp.Core.IntuitRetryPolicy(5, retryInterval);
_AuthenticationCompleted = true;
return true;
}
//
// /// <summary>
// /// Process auth event
// /// </summary>
// /// <param name="accessToken"></param>
// /// <param name="accessTokenSecret"></param>
// /// <param name="realmId"></param>
// /// <param name="dataSource"></param>
// 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
/// <summary>
/// Pre flight check, prompt user as required to repair
/// or add missing settings / failed ones
///
///
/// </summary>
/// <returns></returns>
public static pfstat PreFlightCheck()
{
IntegrationLog.Log(QBID, "PFC: start");
if (!_AuthenticationCompleted)
{
IntegrationLog.Log(QBID, "PFC: Authenticating...");
if (!StartAuthorization())
{
//user cancelled or some other issue, close qboi
IntegrationLog.Log(QBID, "PFC: QuickBooks connection not authorized");
return pfstat.Cancel;
}
}
IntegrationLog.Log(QBID, "PFC: Authentication completed, validating company data");
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();
}
IntegrationLog.Log(QBID, "PFC: Checking integration object...");
IntegrationObjectCheck();
if (ValidateSettings(false) == pfstat.Cancel)
{
IntegrationLog.Log(QBID, "PFC: User settings not completed, user selected cancel");
return pfstat.Cancel;
}
IntegrationLog.Log(QBID, "PFC: Checking cached mappings for validity...");
//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();
}
}
IntegrationLog.Log(QBID, "PFC: pre-flight check completed - OK");
return pfstat.OK;
}
#region PFC AyaNova side
/// <summary>
/// Check if integration object is present in database, if not then
/// add it.
/// </summary>
/// <returns></returns>
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
/// <summary>
/// Validate QB data
/// </summary>
public static pfstat QBValidate()
{
/**
12/28/2018 Error reported by user:
*
* All my clients are set to active but when I connect QBOI to Ayanova it keeps telling me that all the clients are set to inactive.
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: Nov 20, 2018 10:38:44 AM
KEY: MigrationSource, VALUE: QuickBooks-Unknown ActiveX
KEY: SubscriptionStatus, VALUE: PAID
KEY: OfferingSku, VALUE: QuickBooks Online Essentials
KEY: PayrollFeature, VALUE: false
KEY: AccountantFeature, VALUE: false
KEY: QBOIndustryType, VALUE: I sell products & services
KEY: ItemCategoriesFeature, VALUE: true
KEY: AssignedTime, VALUE: 11/09/2018 11:50:11
*/
/*
* 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??
*
*
*/
RefreshTokens();
try
{
QueryService<q.CompanyInfo> entityQuery = new QueryService<q.CompanyInfo>(SC);
List<q.CompanyInfo> comp = entityQuery.ExecuteIdsQuery("SELECT * FROM CompanyInfo").ToList<q.CompanyInfo>();
//case 3520
bool dupeKeyFoundInCompanyInfo = false;
//case 3520 redux 12/31/2018
//working theory is that this is getting called when it's already been populated by an earlier action so it makes all items appear to be dupes because they were already set in qbcompprefs
QCompOtherPrefs.Clear();
//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();//removed as part of oauth2 switch
throw;
}
}
#endregion
#region PFC Check if user settings are completed and valid
//case 3267
public static string TRANSACTION_CLASS_NO_CLASS_SELECTED = "<GZNOCLASS>";
/// <summary>
/// Validate the users preferences
/// if any are missing or invalid prompt for them
/// </summary>
/// <returns></returns>
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;
/// <summary>
/// AyaNova ClientPickList
/// </summary>
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;
/// <summary>
/// AyaNova vendor list
/// </summary>
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;
/// <summary>
/// AyaNova rate list
/// </summary>
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;
/// <summary>
/// AyaNova part list
/// </summary>
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
/// <summary>
/// Populate or repopulate the list of
/// </summary>
public static void PopulateQBListCache()
{
//Get the cached QB data
Waiting w = new Waiting();
w.Show();
w.Ops = "Reading from QuickBooks Online...";
RefreshTokens();
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;
/// <summary>
/// qb items
/// </summary>
public static DataTable QBItems
{
get
{
return _dtQBItems;
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="QBItemID"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Populate the cached qb data
/// billable
/// </summary>
private static void PopulateQBItemCache()
{
RefreshTokens();
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<q.Item> entityQuery = new QueryService<q.Item>(SC);
while (!bDone)
{
//GET first 1000
List<q.Item> items = entityQuery.ExecuteIdsQuery("select * from Item where Type='" + s + "' startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Item>();
//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;
/// <summary>
/// QB Transaction Classes
/// </summary>
public static DataTable QBClasses
{
get
{
return _dtQBClasses;
}
}
/// <summary>
/// Populate the cached qb data
/// billable
/// </summary>
private static void PopulateQBClassCache()
{
RefreshTokens();
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<q.Class> classQuery = new QueryService<q.Class>(SC);
while (!bDone)
{
//GET first 1000
List<q.Class> classes = classQuery.ExecuteIdsQuery("select name from Class startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Class>();
//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;
/// <summary>
/// QB Transaction Templates
/// </summary>
public static DataTable QBInvoiceTemplates
{
get
{
return _dtQBInvoiceTemplates;
}
}
/// <summary>
/// Populate the cached qb data
/// Invoice templates
/// </summary>
private static void PopulateQBInvoiceTemplates()
{
RefreshTokens();
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)
{
RefreshTokens();
try
{
QueryService<q.Customer> qs = new QueryService<q.Customer>(SC);
List<q.Customer> items = qs.ExecuteIdsQuery("select synctoken from Customer where id = '" + customerid + "'").ToList<q.Customer>();
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;
/// <summary>
/// QB Transaction Clients
/// </summary>
public static DataTable QBClients
{
get
{
return _dtQBClients;
}
}
#region Address structure
/// <summary>
/// Address properties
/// </summary>
public struct Address
{
public string DeliveryAddress;
public string City;
public string StateProv;
public string Country;
public string Postal;
}
#endregion
/// <summary>
/// Populate the cached qb data
/// of customers / clients
/// </summary>
private static void PopulateQBClientCache()
{
RefreshTokens();
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<q.Customer> qs = new QueryService<q.Customer>(SC);
while (!bDone)
{
//GET first 1000
List<q.Customer> items = qs.ExecuteIdsQuery("select * from Customer startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Customer>();
//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;
}
}
/// <summary>
/// handle null phones, just get the digits
/// </summary>
/// <param name="ph"></param>
/// <returns></returns>
private static string ProcessQBPhone(q.TelephoneNumber ph)
{
if (ph == null)
return string.Empty;
else
return ph.FreeFormNumber;
}
/// <summary>
/// handle null phones, just get the digits
/// </summary>
/// <param name="ph"></param>
/// <returns></returns>
private static string ProcessQBEmail(q.EmailAddress em)
{
if (em == null)
return string.Empty;
else
return em.Address;
}
/// <summary>
/// Take a qb address and return an AyaNova friendly
/// address structure
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
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;
/// <summary>
/// QB Vendors
/// </summary>
public static DataTable QBVendors
{
get
{
return _dtQBVendors;
}
}
/// <summary>
/// Populate the cached qb data
/// of Vendors
/// </summary>
private static void PopulateQBVendorCache()
{
RefreshTokens();
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<q.Vendor> qs = new QueryService<q.Vendor>(SC);
while (!bDone)
{
//GET first 1000
List<q.Vendor> items = qs.ExecuteIdsQuery("select * from Vendor startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Vendor>();
//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;
/// <summary>
/// QB Transaction Classes
/// </summary>
public static DataTable QBAccounts
{
get
{
return _dtQBAccounts;
}
}
/// <summary>
/// Populate the cached qb account list data
/// </summary>
private static void PopulateQBAccountCache()
{
RefreshTokens();
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<q.Account> qs = new QueryService<q.Account>(SC);
while (!bDone)
{
//GET first 1000
List<q.Account> items = qs.ExecuteIdsQuery("select * from Account startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Account>();
//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;
/// <summary>
/// QB terms
/// </summary>
public static DataTable QBTerms
{
get
{
return _dtQBTerms;
}
}
/// <summary>
/// Populate the cached qb terms list data
/// </summary>
private static void PopulateQBTermsCache()
{
RefreshTokens();
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<q.Term> qs = new QueryService<q.Term>(SC);
while (!bDone)
{
//GET first 1000
List<q.Term> items = qs.ExecuteIdsQuery("select * from Term startposition " + nStart.ToString() + " maxresults 1000").ToList<q.Term>();
//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;
/// <summary>
/// QB tax codes
/// </summary>
public static DataTable QBTaxCodes
{
get
{
return _dtQBTaxCodes;
}
}
public static string TAX_CODE_ID_NO_TAX = "<GZNOTAX>";
/// <summary>
/// Populate the cached qb list data
/// </summary>
private static void PopulateQBTaxCodesCache()
{
RefreshTokens();
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<q.TaxCode> qs = new QueryService<q.TaxCode>(SC);
if (QUSA)
{
_dtQBTaxCodes.Rows.Add(
new object[] {
TAX_CODE_ID_NO_TAX,
"< NOT TAXABLE >"
});
}
while (!bDone)
{
//GET first 1000
List<q.TaxCode> items = qs.ExecuteIdsQuery("select * from TaxCode startposition " + nStart.ToString() + " maxresults 1000").ToList<q.TaxCode>();
//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;
}
}
/// <summary>
/// Populate the cached qb data
/// </summary>
private static void PopulateQBPreferencesCache()
{
RefreshTokens();
try
{
QueryService<q.Preferences> qs = new QueryService<q.Preferences>(SC);
//GET the one item
List<q.Preferences> items = qs.ExecuteIdsQuery("select * from preferences").ToList<q.Preferences>();
_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;
// }
//}
///// <summary>
///// Populate the cached qb data
///// </summary>
//private static void PopulateQBEntitlementCache()
//{
// try
// {
// QueryService<q.Entitlement> qs = new QueryService<q.Entitlement>(SC);
// //GET the one item
// List<q.Entitlement> items = qs.ExecuteIdsQuery("select * from entitlements").ToList<q.Entitlement>();
// _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<Guid> 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
/// <summary>
/// Import the indicated customer
/// to an AyaNova client record
/// </summary>
/// <param name="QuickBooksID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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
/// <summary>
/// Import the indicated Vendor
/// to an AyaNova vendor record
/// </summary>
/// <param name="QuickBooksID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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
/// <summary>
/// Import the indicated QB Item
/// to an AyaNova Rate record
/// </summary>
/// <param name="QuickBooksID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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<Guid> 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"];
}
/// <summary>
/// Import the indicated QB Item
/// to an AyaNova Part record
/// </summary>
/// <param name="QuickBooksID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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<Guid> objectIDList)
{
foreach (Guid g in objectIDList)
{
try
{
Client c = Client.GetItemNoMRU(g);
RefreshQBClientFromAyaNova(c);
}
catch { };
}
}
/// <summary>
/// Refresh the indicated AyaNova client
/// to it's linked QuickBooks customer record
/// </summary>
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 -------------
RefreshTokens();
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
/// <summary>
/// Used for both update and create
/// </summary>
/// <param name="c"></param>
/// <param name="im"></param>
/// <param name="strSyncToken"></param>
/// <param name="ca"></param>
/// <param name="sName"></param>
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<Guid> 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();
}
}
/// <summary>
/// Import the indicated client
/// to QuickBooks customer record
/// </summary>
/// <param name="ClientID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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================================
RefreshTokens();
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
/// <summary>
/// Copy the contents of one address to the other
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
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);
}
///// <summary>
///// Convenience helper for copying address fields above
///// </summary>
///// <param name="from"></param>
///// <param name="to"></param>
//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
/// <summary>
/// Used for both update and create
/// </summary>
/// <param name="c"></param>
/// <param name="im"></param>
/// <param name="strSyncToken"></param>
/// <param name="ca"></param>
/// <param name="sName"></param>
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
/// <summary>
/// Import the indicated Vendor
/// to QuickBooks Vendor record
/// </summary>
/// <param name="VendorID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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================================
RefreshTokens();
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
/// <summary>
/// Refresh the list of AyaNova parts to their linked QuickBooks item records
/// </summary>
/// <param name="objectIDList"></param>
public static void RefreshQBPartFromAyaNova(List<Guid> objectIDList)
{
foreach (Guid g in objectIDList)
{
try
{
Part c = Part.GetItem(g);
RefreshQBPartFromAyaNova(c);
}
catch { };
}
}
/// <summary>
/// Refresh the indicated AyaNova part
/// to it's linked QuickBooks item record
/// </summary>
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 -------------
RefreshTokens();
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
/// <summary>
/// Import a list of ayanova parts to QuickBooks
/// </summary>
/// <param name="objectIDList"></param>
public static void ImportAyaPart(List<Guid> 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();
}
}
/// <summary>
/// Import the indicated part
/// to QuickBooks Online item record
/// </summary>
/// <param name="VendorID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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================================
RefreshTokens();
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;
}
/// <summary>
/// Import the indicated service rate
/// to QuickBooks item record
/// </summary>
/// <param name="VendorID"></param>
/// <param name="alErrors">An arraylist to hold strings indicating errors on fail</param>
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================================
RefreshTokens();
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
}
/// <summary>
/// Mismatch properties
/// A structure for storing mismatches identified
///so user can resolve them.
/// </summary>
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;
}
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="WorkorderID">Id of workorder being scanned</param>
/// <param name="MisMatches">An arraylist of mismatch objects</param>
/// <param name="PriceOverrides">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</param>
/// <returns>True if all links ok, false if there are any mismatches at all</returns>
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
/// <summary>
/// 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
/// </summary>
/// <param name="alWorkorders"></param>
/// <returns></returns>
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<q.Line> AllInvoiceLines = new List<q.Line>();
//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================================
RefreshTokens();
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));
}
}
/// <summary>
/// Add text to the invoice
/// chopping into chunks less than the max 4095 characters limit
/// as necessary
/// </summary>
private static void InvoiceAddText(List<q.Line> lineList, string Text)
{
if (Text == null || Text == "") return;
List<string> textLines = new List<string>();
//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);
}
}
/// <summary>
/// Add charge line to the invoice
///
/// </summary>
private static void InvoiceAddCharge(List<q.Line> 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 -------------
RefreshTokens();
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
{
RefreshTokens();
QueryService<q.Item> qs = new QueryService<q.Item>(SC);
List<q.Item> items = qs.ExecuteIdsQuery("select synctoken from Item where id = '" + QBListID + "'").ToList<q.Item>();
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();
}
/// <summary>
/// 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
///
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
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;
}
}
//--------------------------------------------------------------------------------
}
}