909 lines
35 KiB
C#
909 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using Newtonsoft.Json.Linq;
|
|
using Interop.QBFC15;
|
|
using System.Text.RegularExpressions;
|
|
using System.Windows.Forms;
|
|
|
|
namespace AyaNovaQBI
|
|
{
|
|
internal class util
|
|
{
|
|
#region API stuff
|
|
public static Guid QBI_INTEGRATION_ID
|
|
{
|
|
get { return new Guid("{82CD3609-4601-4C1A-9633-7836F92D2D06}"); }
|
|
}
|
|
public const string TEST_ROUTE = "notify/hello";
|
|
public const string API_BASE_ROUTE = "api/v8/";
|
|
private const int MAX_TRIES = 3;//max times to retry an api call before giving up
|
|
private const int API_RETRY_DELAY = 3000;//pause in ms before retrying api call
|
|
public static int HTTPCLIENT_TIMEOUT_SECONDS = 100;//changed by the setting in ops anyway, just a in-case sensible default here
|
|
public static HttpClient client = null;
|
|
//url once known to be good
|
|
internal static string ApiBaseUrl { get; set; }
|
|
//auth processes url for api and this is the best guess as to the client url to use for notification / help links etc
|
|
internal static string GuessClientUrl { get; set; }
|
|
|
|
internal static string JWT { get; set; }
|
|
|
|
// internal static long AyaNovaUserId { get; set; } //probably don't need this, if I do then some code will need to be added to decode the JWT or at the server to get my currently logged in USER ID
|
|
internal static string AyaNovaUserName { get; set; }
|
|
internal static AuthorizationRoles AyaNovaUserRoles { get; set; }
|
|
internal static UserType AyaNovaUserType { get; set; }
|
|
|
|
internal static AyaNovaLicense ALicense { get; set; } = null;
|
|
|
|
internal static Integration QBIntegration { get; set; } = null;
|
|
internal static bool LOG_AVAILABLE { get; set; } = false;
|
|
|
|
|
|
|
|
|
|
public static void InitClient()
|
|
{
|
|
if (client != null)
|
|
{
|
|
client.Dispose();
|
|
client = null;
|
|
}
|
|
client = new HttpClient();
|
|
client.Timeout = TimeSpan.FromSeconds(HTTPCLIENT_TIMEOUT_SECONDS);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Only a return value of "OK" is ok
|
|
/// </summary>
|
|
/// <param name="serverUrl"></param>
|
|
/// <returns></returns>
|
|
public static async Task<string> InitAndConfirmAddressAsync(string serverUrl)
|
|
{
|
|
ApiBaseUrl = serverUrl;
|
|
InitClient();
|
|
try
|
|
{
|
|
// TimeSpan tsDefault = client.Timeout;
|
|
// client.Timeout = new TimeSpan(0, 0, 20);
|
|
HttpResponseMessage response = await client.GetAsync(serverUrl + TEST_ROUTE);
|
|
// client.Timeout = tsDefault;
|
|
if (response.IsSuccessStatusCode)
|
|
return "OK";
|
|
else
|
|
return $"Failed: {response.StatusCode.ToString()}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
while (ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
return $"Failed exception: \r\n{ex.Message}";
|
|
}
|
|
}
|
|
|
|
|
|
public async static Task<bool> AuthenticateAsync(string login, string password = null)
|
|
{
|
|
InitClient();
|
|
|
|
if (password == null)
|
|
password = login;
|
|
|
|
dynamic creds = new JObject();
|
|
creds.login = login;
|
|
creds.password = password;
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + "auth");
|
|
requestMessage.Content = new StringContent(creds.ToString(), System.Text.Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
|
|
throw new Exception($"Authentication error, route: AUTH\r\nError:{Err}\r\nInner error:{InnerErr}");
|
|
}
|
|
|
|
var a = new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(await response.Content.ReadAsStringAsync()) };
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
|
|
bool tfa = a.ObjectResponse["data"]["tfa"].Value<bool>();
|
|
if (tfa == true)
|
|
{
|
|
//Get temp token from response
|
|
|
|
var tempToken = a.ObjectResponse["data"]["tt"].Value<string>();
|
|
|
|
//get 2fa code and send it in
|
|
do
|
|
{
|
|
tfa t = new tfa();
|
|
if (t.ShowDialog() == System.Windows.Forms.DialogResult.Cancel) return false;
|
|
string tfaPin = t.TFAPin;
|
|
|
|
dynamic tfaCreds = new JObject();
|
|
tfaCreds.pin = tfaPin;
|
|
tfaCreds.tempToken = tempToken;
|
|
try
|
|
{
|
|
var tfaResponse = await TryPostAsync("auth/tfa-authenticate", tfaCreds.ToString(Newtonsoft.Json.Formatting.None));//trypost is no delay
|
|
if (ProcessLoginResponse(tfaResponse)) return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!ex.Message.Contains("2003"))//if not an authentication error (bad pin) then throw it back up for display
|
|
throw ex;
|
|
//otherwise eat it and let them re-enter the pin again to mirror how ayanova web client works
|
|
|
|
}
|
|
|
|
|
|
} while (true);
|
|
|
|
}
|
|
else
|
|
{
|
|
return ProcessLoginResponse(a);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
if (a.ObjectResponse != null && a.ObjectResponse.ContainsKey("error"))
|
|
{
|
|
|
|
var errCode = a.ObjectResponse["error"]["code"].Value<string>();
|
|
if (errCode.Contains("2003")) return false;//simple authentication error
|
|
//some other error, possibly expired ayanova license etc, show it so it's clear why the login failed so they known it's not a creds issue
|
|
var errMessage = a.ObjectResponse["error"]["message"].Value<string>();
|
|
throw new Exception($"Code: {errCode} - {errMessage}");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private static bool ProcessLoginResponse(ApiResponse a)
|
|
{
|
|
if (a.ObjectResponse == null) return false;
|
|
if (!a.HttpResponse.IsSuccessStatusCode)
|
|
{
|
|
return false;
|
|
}
|
|
if (a.ObjectResponse["data"]["l"].Value<bool>())//license lockout
|
|
{
|
|
throw new Exception("Server login from QBI is disabled due to AyaNova license issue");
|
|
}
|
|
|
|
JWT = a.ObjectResponse["data"]["token"].Value<string>();
|
|
|
|
AyaNovaUserName = a.ObjectResponse["data"]["name"].Value<string>();
|
|
AyaNovaUserRoles = (AuthorizationRoles)(int.Parse(a.ObjectResponse["data"]["roles"].Value<string>()));
|
|
AyaNovaUserType = (UserType)(int.Parse(a.ObjectResponse["data"]["usertype"].Value<string>()));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> GetAsync(string route)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryGetAsync(route);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception($"API call failed after {MAX_TRIES.ToString()} attempts", FirstException);
|
|
}
|
|
|
|
private async static Task<ApiResponse> TryGetAsync(string route)
|
|
{
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + route);
|
|
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception($"GET error, route: {route}\r\nError:{Err}\r\nInner error:{InnerErr}\r\nStack:{ex.StackTrace}");
|
|
}
|
|
|
|
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new Exception($"GET error, code: {(int)response.StatusCode}, route: {route}\r\n{responseAsString}\r\n{response.ReasonPhrase}");
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> PostAsync(string route, dynamic d)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPostAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception($"API call failed after {MAX_TRIES.ToString()} attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> PostAsync(string route, string s = null)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPostAsync(route, s);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception($"API call failed after {MAX_TRIES.ToString()} attempts", FirstException);
|
|
}
|
|
|
|
|
|
internal async static Task<ApiResponse> TryPostAsync(string route, string postJson = null)
|
|
{
|
|
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + route);
|
|
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
if (!string.IsNullOrWhiteSpace(postJson))
|
|
requestMessage.Content = new StringContent(postJson, System.Text.Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception($"POST error, route: {route}\r\nError:{Err}\r\nInner error:{InnerErr}\r\nStack:{ex.StackTrace}\r\nPOSTED OBJECT:\r\n{postJson}");
|
|
}
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(postJson))
|
|
postJson = "n/a";
|
|
throw new Exception($"POST error, code: {(int)response.StatusCode}, route: {route}\r\n{responseAsString}\r\n{response.ReasonPhrase}\r\nPOSTED OBJECT:\r\n{postJson}");
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="jsonString"></param>
|
|
/// <returns></returns>
|
|
public static JObject Parse(string jsonString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(jsonString))
|
|
{
|
|
return null;
|
|
}
|
|
return JObject.Parse(jsonString);
|
|
}
|
|
|
|
|
|
public static long IdFromResponse(ApiResponse a)
|
|
{
|
|
return a.ObjectResponse["data"]["id"].Value<long>();
|
|
}
|
|
|
|
public static uint CTokenFromResponse(ApiResponse a)
|
|
{
|
|
return a.ObjectResponse["data"]["concurrency"].Value<uint>();
|
|
}
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> PutAsync(string route, dynamic d)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPutAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception($"API call failed after {MAX_TRIES.ToString()} attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> PutAsync(string route)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPutAsync(route);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception($"API call failed after {MAX_TRIES.ToString()} attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> TryPutAsync(string route, string putJson = null)
|
|
{
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + route);
|
|
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
if (!string.IsNullOrWhiteSpace(putJson))
|
|
requestMessage.Content = new StringContent(putJson, System.Text.Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception($"PUT error, route: {route}\r\nError:{Err}\r\nInner error:{InnerErr}\r\nStack:{ex.StackTrace}\r\nPOSTED OBJECT:\r\n{putJson}");
|
|
}
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(putJson))
|
|
putJson = "n/a";
|
|
throw new Exception($"PUT error, code: {(int)response.StatusCode}, route: {route}\r\n{responseAsString}\r\n{response.ReasonPhrase}\r\nPUT OBJECT:\r\n{putJson}");
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
public class ApiResponse
|
|
{
|
|
public HttpResponseMessage HttpResponse { get; set; }
|
|
public JObject ObjectResponse { get; set; }
|
|
public string CompactResponse
|
|
{
|
|
get
|
|
{
|
|
return ObjectResponse.ToString(Newtonsoft.Json.Formatting.None);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region QB STUFF
|
|
|
|
|
|
public static string QCountry = "US";
|
|
public static double QVersion = 1.1;
|
|
public static string QCompanyFile = "";
|
|
public static string QCompanyName = "";
|
|
public static string sLastRequestXML = "";
|
|
|
|
public enum pfstat
|
|
{
|
|
OK = 0,
|
|
Failed = 1,
|
|
Cancel = 2
|
|
}
|
|
|
|
public static List<InvoiceableItem> GetInvoiceableItems()
|
|
{
|
|
var random = new Random();
|
|
var l = new List<InvoiceableItem>();
|
|
for (int i = 1; i < random.Next(25, 100); i++)
|
|
l.Add(new InvoiceableItem { Customer = $"Customer {random.Next(1, 5)}", Linked = random.Next(2) == 1, Project = $"project {i}", ServiceDate = DateTime.Now.ToString("g"), ServiceNumber = (40 + i).ToString(), Status = $"Waiting to be invoiced", StatusColor = "FF00FFAA", WorkorderId = 4 });
|
|
|
|
return l.OrderBy(x => x.Customer)
|
|
.ThenBy(x => x.ServiceNumber)
|
|
.ThenBy(x => x.ServiceDate)
|
|
.ToList();
|
|
|
|
}
|
|
|
|
|
|
public static async Task<bool> InitializeQBI(StringBuilder initErrors)
|
|
{
|
|
//COPY most of this code from qbi v7 becuase it has a lot of edge cases in it and it's complex and thorough, but break it into abstracted bits so can be replicated in other accounting add-on's more easily
|
|
|
|
//This is pre-pfc block of stuff that doesn't map well from v7 qbi plugin to here so replicate it in spirit here but not much is copyable just the concepts
|
|
//LOGIN to v8 first
|
|
auth d = new auth();
|
|
if (d.ShowDialog() == System.Windows.Forms.DialogResult.Cancel)
|
|
return false;
|
|
|
|
//ACCOUNTING ROLE?
|
|
if (!AyaNovaUserRoles.HasFlag(AuthorizationRoles.Accounting))
|
|
{
|
|
initErrors.AppendLine($"User must have the \"Accounting\" Role to use QBI\r\n");
|
|
return false;
|
|
}
|
|
|
|
|
|
//Get license
|
|
var r = await GetAsync("license");
|
|
ALicense = r.ObjectResponse["data"]["license"].ToObject<AyaNovaLicense>();
|
|
|
|
//UNEXPIRED AYANOVA LICENSE?
|
|
if (ALicense.licenseWillExpire && ALicense.licenseExpiration < DateTime.UtcNow)
|
|
{
|
|
initErrors.AppendLine($"AyaNova license has expired {ALicense.licenseExpiration.ToLocalTime().ToString("g")}");
|
|
return false;
|
|
}
|
|
|
|
//QBI LICENSED?
|
|
if (ALicense.features.FirstOrDefault(z => z.Feature == "QBI") == null)
|
|
{
|
|
initErrors.AppendLine("QBI not licensed");
|
|
return false;
|
|
}
|
|
|
|
//BUILD DATE VERSION ALLOWED?
|
|
if (ALicense.maintenanceExpiration < Timestamp.BuildAt)
|
|
{
|
|
initErrors.AppendLine($"NOT LICENSED!\r\n\r\nThis QBI plugin was built {Timestamp.BuildAt.ToString("g")}\r\nbut the licensed support and updates subscription has ended on {ALicense.maintenanceExpiration.ToLocalTime().ToString("g")}\r\n\r\nDowngrade back to your previous licensed QBI version or\r\npurchase a support and updates subscription to continue using this version of QBI.");
|
|
return false;
|
|
}
|
|
|
|
//PFC - integration object check (fetch or create if not present)
|
|
//Need this early so can log any issues with other aspects
|
|
if (!await IntegrationCheck(initErrors))
|
|
return false;
|
|
LOG_AVAILABLE = true;
|
|
|
|
|
|
await IntegrationLog($"PFC: AyaNova user \"{AyaNovaUserName}\" starting QBI, pre-flight check (PFC) commencing");
|
|
|
|
//QB CONNECTION validation and setup
|
|
try
|
|
{
|
|
if(await QBValidate() == pfstat.Cancel)
|
|
{
|
|
await IntegrationLog("PFC: Unable to validate QuickBooks connection, user selected cancel");
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
await IntegrationLog($"PFC: QB Company name= {QCompanyName}, QB Country version={QCountry}, QBVersion={QVersion}, Companyfile={QCompanyFile}");
|
|
//once connected collect the country, version we are dealing with (Util.qbValidate)
|
|
//confirm qb is 2008 or newer and bail if not (util.qbvalidate)
|
|
//cache company name and other qb info
|
|
//PFC - PopulateQBListCache()
|
|
//PFC - PopulateAyaListCache()
|
|
|
|
|
|
//PFC - Validate settings, create if necessary (Util.ValidateSettings()) and save
|
|
//TODO: if QBIntegration.IntegrationData==null then nothing is set yet and it's fresh so trigger the setup stuff
|
|
|
|
//PFC - verify integration mapped objects still exist at each end (Util.PreFlightCheck() line 199)
|
|
//DONE
|
|
}
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
initErrors.AppendLine($"QuickBooks connection validation failed before connecting\r\n{ex.Message}");
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Ensure existance of QBI Integration object
|
|
/// </summary>
|
|
/// <param name="initErrors"></param>
|
|
/// <returns></returns>
|
|
public static async Task<bool> IntegrationCheck(StringBuilder initErrors)
|
|
{
|
|
ApiResponse r = null;
|
|
try
|
|
{
|
|
r = await GetAsync($"integration/exists/{QBI_INTEGRATION_ID}");
|
|
|
|
if (r.ObjectResponse["data"].Value<bool>() == false)
|
|
{
|
|
//doesn't exist, need to create it now
|
|
QBIntegration = new Integration();
|
|
QBIntegration.IntegrationAppId = QBI_INTEGRATION_ID;
|
|
QBIntegration.Active = true;
|
|
QBIntegration.Name = "QBI - QuickBooks Desktop integration";
|
|
r = await PostAsync("integration", Newtonsoft.Json.JsonConvert.SerializeObject(QBIntegration));
|
|
QBIntegration.Id = IdFromResponse(r);
|
|
await IntegrationLog("AyaNova QBI Integration installed to AyaNova");
|
|
}
|
|
else
|
|
{
|
|
//Exists, fetch it check if active then we're done here
|
|
r = await GetAsync($"integration/{QBI_INTEGRATION_ID}");
|
|
QBIntegration = r.ObjectResponse["data"].ToObject<Integration>();
|
|
|
|
if (!QBIntegration.Active)
|
|
{
|
|
initErrors.AppendLine("QBI Integration is currently deactivated and can not be used\r\nThis setting can be changed in AyaNova in the Administration section -> Integrations -> QuickBooks Desktop integration record\r\nSet to active and save to enable QBI");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
initErrors.AppendLine("Error fetching QBI Integration object");
|
|
initErrors.AppendLine(ex.Message);
|
|
initErrors.AppendLine(r.CompactResponse);
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public static async Task IntegrationLog(string logLine)
|
|
{
|
|
|
|
await PostAsync("integration/log", Newtonsoft.Json.JsonConvert.SerializeObject(new NameIdItem { Id = QBIntegration.Id, Name = logLine }));
|
|
|
|
}
|
|
|
|
|
|
#region PFC QB side
|
|
/// <summary>
|
|
/// Open QB connection
|
|
/// gather info required for future
|
|
/// transactions
|
|
/// </summary>
|
|
public async static Task<pfstat> QBValidate()
|
|
{
|
|
// We want to know if we begun a session so we can end it if an
|
|
// error happens
|
|
bool booSessionBegun = false;
|
|
bool bConnected = false;
|
|
// Create the session manager object using QBFC
|
|
QBSessionManager sessionManager = new QBSessionManager();
|
|
|
|
while (!booSessionBegun)
|
|
{
|
|
try
|
|
{
|
|
sessionManager.OpenConnection2("", "AyaNova QBI", ENConnectionType.ctLocalQBDLaunchUI);
|
|
bConnected = true;
|
|
sessionManager.BeginSession("", ENOpenMode.omDontCare);
|
|
booSessionBegun = true;
|
|
|
|
}
|
|
catch (System.Runtime.InteropServices.COMException ex)
|
|
{
|
|
if (bConnected)
|
|
sessionManager.CloseConnection();
|
|
if (ex.ErrorCode == -2147220458 || ex.ErrorCode == -2147220472 || ex.Message.Contains("Could not start"))
|
|
{
|
|
var msg = "QuickBooks doesn't appear to be running on this computer.\r\n" +
|
|
"Start QuickBooks and open your company file now before proceeding.";
|
|
|
|
await IntegrationLog($"PFC: {msg}");
|
|
|
|
if (MessageBox.Show(
|
|
msg,
|
|
"AyaNova QBI: Pre flight check",
|
|
MessageBoxButtons.RetryCancel, MessageBoxIcon.Information) == DialogResult.Cancel)
|
|
{
|
|
await IntegrationLog($"PFC: User opted to cancel");
|
|
return pfstat.Cancel;
|
|
}
|
|
|
|
await IntegrationLog($"PFC: User opted to retry");
|
|
|
|
|
|
}
|
|
else
|
|
{
|
|
await IntegrationLog($"PFC: QBValidate connect unanticipated exception: {ex.Message}\r\nError code:{ex.ErrorCode.ToString()}");
|
|
MessageBox.Show($"{ex.Message}\r\nError code:{string.Format("(HRESULT:0x{0:X8})", ex.ErrorCode)}");
|
|
return pfstat.Cancel;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
|
|
//Get the country and latest version supported
|
|
QVersion = 0;
|
|
QCountry = "US";//default
|
|
|
|
string[] versions = sessionManager.QBXMLVersionsForSession;
|
|
double vers = 0;
|
|
|
|
Regex rxVersion = new Regex("[0-9.,]+", RegexOptions.Multiline | RegexOptions.Compiled);
|
|
|
|
foreach (string s in versions)
|
|
{
|
|
if (s.StartsWith("CA") || s.StartsWith("ca"))
|
|
{
|
|
QCountry = "CA";
|
|
|
|
}
|
|
else if (s.StartsWith("UK") || s.StartsWith("uk"))
|
|
{
|
|
QCountry = "UK";
|
|
|
|
}
|
|
|
|
//case 262
|
|
//strip out only numeric bit regardless of what text is in there
|
|
//including commas if it's french canadian and using a comma instead of a decimal point
|
|
//(the safe to double will handle the comma if present)
|
|
string strVersionNumber = rxVersion.Match(s).Value;
|
|
vers = SafeToDouble(strVersionNumber);
|
|
|
|
if (vers > QVersion)
|
|
{
|
|
QVersion = vers;
|
|
}
|
|
}
|
|
|
|
if (QVersion < 6.0)
|
|
{
|
|
var msg = $"You seem to be running QuickBooks older than 2008\r\nYou must update to 2008 or higher before you can use AyaNova QBI.\r\n\r\n(If you are running QuickBooks 2008 or higher and still getting this error, ensure that you are also using QBFC15 or higher)\r\n\r\nVERSION FOUND = {QVersion}";
|
|
await IntegrationLog($"PFC: Failed - {msg}");
|
|
CopyableMessageBox cp = new CopyableMessageBox(msg);
|
|
cp.ShowDialog();
|
|
return pfstat.Failed;
|
|
}
|
|
|
|
|
|
//Get the company file to open
|
|
QCompanyFile = sessionManager.GetCurrentCompanyFileName();
|
|
|
|
// if(QCountry=="US")
|
|
// {
|
|
//Get company data
|
|
// IY: Get the RequestMsgSet based on the correct QB Version
|
|
IMsgSetRequest requestSet = getLatestMsgSetRequest(sessionManager);
|
|
|
|
// IY: Initialize the message set request object
|
|
requestSet.Attributes.OnError = ENRqOnError.roeStop;
|
|
|
|
// IY: Add the request to the message set request object
|
|
ICompanyQuery cq = requestSet.AppendCompanyQueryRq();
|
|
|
|
IMsgSetResponse responseSet = sessionManager.DoRequests(requestSet);
|
|
IResponse response = responseSet.ResponseList.GetAt(0);
|
|
if (response.StatusCode != 0)
|
|
{
|
|
await IntegrationLog($"PFC: Failed Company query:{response.StatusMessage}, {response.StatusCode.ToString()}");
|
|
throw new ApplicationException($"PFC: Failed Company query:{response.StatusMessage}, {response.StatusCode.ToString()}");
|
|
|
|
}
|
|
|
|
ICompanyRet cl = response.Detail as ICompanyRet;
|
|
QCompanyName = ProcessQBString(cl.CompanyName);
|
|
requestSet.ClearRequests();
|
|
//----------------
|
|
|
|
|
|
// Close the session and connection with QuickBooks
|
|
sessionManager.EndSession();
|
|
booSessionBegun = false;
|
|
sessionManager.CloseConnection();
|
|
return pfstat.OK;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//MessageBox.Show(ex.Message.ToString() + "\nStack Trace: \n" + ex.StackTrace + "\nExiting the application");
|
|
|
|
await IntegrationLog($"PFC: Failed with exception:{ex.Message}");
|
|
if (booSessionBegun)
|
|
{
|
|
sessionManager.EndSession();
|
|
sessionManager.CloseConnection();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
#endregion pfc qb side
|
|
|
|
|
|
|
|
|
|
|
|
#region QB Specific utils
|
|
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="sessionManager"></param>
|
|
/// <returns></returns>
|
|
public static IMsgSetRequest getLatestMsgSetRequest(QBSessionManager sessionManager)
|
|
{
|
|
// Find and adapt to supported version of QuickBooks
|
|
short qbXMLMajorVer = 0;
|
|
short qbXMLMinorVer = 0;
|
|
|
|
if (QVersion >= 5.0)
|
|
{
|
|
qbXMLMajorVer = 5;
|
|
qbXMLMinorVer = 0;
|
|
}
|
|
else if (QVersion >= 4.0)
|
|
{
|
|
qbXMLMajorVer = 4;
|
|
qbXMLMinorVer = 0;
|
|
}
|
|
else if (QVersion >= 3.0)
|
|
{
|
|
qbXMLMajorVer = 3;
|
|
qbXMLMinorVer = 0;
|
|
}
|
|
else if (QVersion >= 2.0)
|
|
{
|
|
qbXMLMajorVer = 2;
|
|
qbXMLMinorVer = 0;
|
|
}
|
|
else if (QVersion >= 1.1)
|
|
{
|
|
qbXMLMajorVer = 1;
|
|
qbXMLMinorVer = 1;
|
|
}
|
|
else
|
|
{
|
|
qbXMLMajorVer = 1;
|
|
qbXMLMinorVer = 0;
|
|
throw new System.NotSupportedException("QuickBooks 1.0 (2002 initial release) is not supported, use QuickBooks online update feature now.");
|
|
}
|
|
|
|
// Create the message set request object
|
|
IMsgSetRequest requestMsgSet = sessionManager.CreateMsgSetRequest(QCountry, qbXMLMajorVer, qbXMLMinorVer);
|
|
return requestMsgSet;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle null qb string types with "aplomb" :)
|
|
/// </summary>
|
|
/// <param name="qs"></param>
|
|
/// <returns></returns>
|
|
private static string ProcessQBString(IQBStringType qs)
|
|
{
|
|
if (qs == null) return "";
|
|
return qs.GetValue();
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
#endregion qb specific utils
|
|
|
|
|
|
|
|
|
|
#endregion qb stuff
|
|
|
|
|
|
}
|
|
}
|