903 lines
34 KiB
C#
903 lines
34 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\n" +
|
|
"but 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;
|
|
|
|
|
|
//Validate QB connection can be made and open connection and start session with QB (see Util.QBValidate in v7
|
|
try
|
|
{
|
|
|
|
var pfstatus = await QBValidate();
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
initErrors.AppendLine($"QuickBooks connection validation failed before connecting\r\n{ex.Message}");
|
|
|
|
return false;
|
|
}
|
|
//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
|
|
|
|
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));
|
|
var id = IdFromResponse(r);
|
|
}
|
|
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"))
|
|
{
|
|
|
|
if (MessageBox.Show(
|
|
"QuickBooks doesn't appear to be running on this computer.\r\n" +
|
|
"Start QuickBooks and open your company file now before proceeding.",
|
|
"AyaNova QBI: Pre flight check",
|
|
MessageBoxButtons.RetryCancel, MessageBoxIcon.Information) == DialogResult.Cancel) return pfstat.Cancel;
|
|
}
|
|
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)
|
|
{
|
|
await IntegrationLog("PFC: Failed, QuickBooks found is too old, 2008 or higher is required, prompted user to run QB update utility to be able to proceed");
|
|
CopyableMessageBox cp = new CopyableMessageBox(
|
|
|
|
"You seem to be running QuickBooks older than 2008\r\n" +
|
|
"You 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 QBFC7 or higher)\r\n\r\n" +
|
|
"VERSION FOUND = " + QVersion
|
|
|
|
);
|
|
|
|
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();
|
|
//----------------
|
|
// }
|
|
// else
|
|
// QCompanyName=QCompanyFile;
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
}
|