CustomerMYOBPoster.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. using Comal.Classes;
  2. using FastReport.Data;
  3. using InABox.Core;
  4. using InABox.Core.Postable;
  5. using InABox.Poster.MYOB;
  6. using MYOB.AccountRight.SDK.Services;
  7. using MYOB.AccountRight.SDK.Services.Contact;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using System.Net;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. using Customer = Comal.Classes.Customer;
  15. using MYOBCustomer = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Customer;
  16. using MYOBAddress = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Address;
  17. using MYOB.AccountRight.SDK.Contracts.Version2.Sale;
  18. using InABox.Clients;
  19. using InABox.Database;
  20. using MYOB.AccountRight.SDK;
  21. namespace PRS.Shared.Posters.MYOB;
  22. public class CustomerMYOBPosterSettings : MYOBPosterSettings
  23. {
  24. [TextBoxEditor(ToolTip = "The MYOB tax code which should be used when posting customers")]
  25. public string DefaultTaxCode { get; set; }
  26. }
  27. public static class ContactMYOBUtils
  28. {
  29. public static void SplitName(string name, out string firstName, out string lastName)
  30. {
  31. var names = name.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  32. firstName = names.Length > 0 ? names[0] : "";
  33. lastName = names.Length > 1 ? names[1] : "";
  34. }
  35. public static MYOBAddress ConvertAddress(Address address, int location, IContact contact)
  36. {
  37. return new MYOBAddress
  38. {
  39. Location = location,
  40. Street = address.Street.Truncate(255),
  41. City = address.City.Truncate(255),
  42. State = address.State.Truncate(255),
  43. PostCode = address.PostCode.Truncate(11),
  44. Phone1 = contact.Mobile.Truncate(21),
  45. Phone2 = contact.Telephone.Truncate(21),
  46. Email = contact.Email.Truncate(255),
  47. ContactName = contact.Name.Truncate(25)
  48. };
  49. }
  50. }
  51. public class CustomerMYOBAutoRefresher : IAutoRefresher<Customer>
  52. {
  53. public bool ShouldRepost(Customer customer)
  54. {
  55. var shouldRepost = customer.HasOriginalValue(x => x.Name)
  56. || customer.HasOriginalValue(x => x.Code)
  57. || customer.HasOriginalValue(x => x.ABN)
  58. || customer.DefaultContact.HasOriginalValue(x => x.ID)
  59. || customer.Delivery.HasOriginalValue(x => x.Street)
  60. || customer.Delivery.HasOriginalValue(x => x.City)
  61. || customer.Delivery.HasOriginalValue(x => x.State)
  62. || customer.Delivery.HasOriginalValue(x => x.PostCode)
  63. || customer.Postal.HasOriginalValue(x => x.Street)
  64. || customer.Postal.HasOriginalValue(x => x.City)
  65. || customer.Postal.HasOriginalValue(x => x.State)
  66. || customer.Postal.HasOriginalValue(x => x.PostCode);
  67. if (shouldRepost)
  68. {
  69. return true;
  70. }
  71. if(customer.CustomerStatus.HasOriginalValue(x => x.ID))
  72. {
  73. var originalID = customer.CustomerStatus.GetOriginalValue(x => x.ID);
  74. var currentID = customer.CustomerStatus.ID;
  75. var statuses = DbFactory.Provider.Query<CustomerStatus>(
  76. new Filter<CustomerStatus>(x => x.ID).IsEqualTo(originalID).Or(x => x.ID).IsEqualTo(currentID),
  77. Columns.None<CustomerStatus>().Add(x => x.ID).Add(x => x.Active))
  78. .ToArray<CustomerStatus>();
  79. if (statuses.Length == 2 && statuses[0].Active != statuses[1].Active)
  80. {
  81. return true;
  82. }
  83. }
  84. return false;
  85. }
  86. }
  87. public class CustomerMYOBPoster : IMYOBPoster<Customer, CustomerMYOBPosterSettings>, IAutoRefreshPoster<Customer, CustomerMYOBAutoRefresher>
  88. {
  89. public CustomerMYOBPosterSettings Settings { get; set; }
  90. public MYOBGlobalPosterSettings GlobalSettings { get; set; }
  91. public MYOBConnectionData ConnectionData { get; set; }
  92. public bool BeforePost(IDataModel<Customer> model)
  93. {
  94. foreach (var (_, table) in model.ModelTables)
  95. {
  96. table.IsDefault = false;
  97. }
  98. model.SetIsDefault<Customer>(true);
  99. model.SetColumns<Customer>(RequiredColumns());
  100. return true;
  101. }
  102. public static Columns<Customer> RequiredColumns()
  103. {
  104. return Columns.None<Customer>()
  105. .Add(x => x.ID)
  106. .Add(x => x.PostedReference)
  107. .Add(x => x.DefaultContact.Name)
  108. .Add(x => x.Name)
  109. .Add(x => x.Code)
  110. .Add(x => x.CustomerStatus.ID)
  111. .Add(x => x.CustomerStatus.Active)
  112. .Add(x => x.Delivery.Street)
  113. .Add(x => x.Delivery.City)
  114. .Add(x => x.Delivery.State)
  115. .Add(x => x.Delivery.PostCode)
  116. .Add(x => x.Postal.Street)
  117. .Add(x => x.Postal.City)
  118. .Add(x => x.Postal.State)
  119. .Add(x => x.Postal.PostCode)
  120. .Add(x => x.DefaultContact.Mobile)
  121. .Add(x => x.DefaultContact.Telephone)
  122. .Add(x => x.DefaultContact.Email)
  123. .Add(x => x.DefaultContact.Name)
  124. .Add(x => x.DefaultContact.Mobile)
  125. .Add(x => x.ABN);
  126. }
  127. public static Result<Exception> UpdateCustomer(MYOBConnectionData data, CustomerMYOBPosterSettings settings, Customer customer, MYOBCustomer myobCustomer, bool isNew)
  128. {
  129. // Documentation: https://developer.myob.com/api/myob-business-api/v2/contact/customer/
  130. // Since this might be called from some other poster, we need to ensure we have the right columns.
  131. Client.EnsureColumns(customer, RequiredColumns());
  132. ContactMYOBUtils.SplitName(customer.DefaultContact.Name, out var firstName, out var lastName);
  133. myobCustomer.CompanyName = customer.Name.Truncate(50);
  134. myobCustomer.FirstName = firstName.Truncate(30);
  135. myobCustomer.LastName = lastName.Truncate(20);
  136. myobCustomer.IsIndividual = false;
  137. myobCustomer.DisplayID = customer.Code.Truncate(15);
  138. // If there is not customer status, we will use default to Active = true.
  139. myobCustomer.IsActive = customer.CustomerStatus.ID == Guid.Empty || customer.CustomerStatus.Active;
  140. myobCustomer.Addresses =
  141. [
  142. ContactMYOBUtils.ConvertAddress(customer.Postal, 2, customer.DefaultContact),
  143. ContactMYOBUtils.ConvertAddress(customer.Delivery, 1, customer.DefaultContact)
  144. ];
  145. // Notes =
  146. // PhotoURI =
  147. // RowVersion =
  148. myobCustomer.SellingDetails ??= new();
  149. myobCustomer.SellingDetails.SaleLayout = InvoiceLayoutType.NoDefault;
  150. // myobCustomer.SellingDetails.PrintedFOrm =
  151. myobCustomer.SellingDetails.InvoiceDelivery = DocumentAction.PrintAndEmail;
  152. // myobCustomer.SellingDetails.IncomeAccount =
  153. // myobCustomer.SellingDetails.ReceiptMemo =
  154. // myobCustomer.SellingDetails.SalesPerson =
  155. // myobCustomer.SellingDetails.SaleComment =
  156. // myobCustomer.SellingDetails.ShippingMethod =
  157. // myobCustomer.SellingDetails.HourlyBillRate =
  158. // myobCustomer.SellingDetails.ABNBranch =
  159. myobCustomer.SellingDetails.ABN = customer.ABN.Truncate(14);
  160. if (isNew)
  161. {
  162. if (settings.DefaultTaxCode.IsNullOrWhiteSpace())
  163. {
  164. throw new PostFailedMessageException("Default tax code has not been set up.");
  165. }
  166. else if(data.GetMYOBTaxCodeUID(settings.DefaultTaxCode).Get(out var taxID, out var error))
  167. {
  168. if (taxID == Guid.Empty)
  169. {
  170. return Result.Error(new Exception($"Failed to find TaxCode in MYOB with code '{settings.DefaultTaxCode}'"));
  171. }
  172. myobCustomer.SellingDetails.TaxCode ??= new();
  173. myobCustomer.SellingDetails.TaxCode.UID = taxID;
  174. myobCustomer.SellingDetails.FreightTaxCode ??= new();
  175. myobCustomer.SellingDetails.FreightTaxCode.UID = taxID;
  176. }
  177. else
  178. {
  179. CoreUtils.LogException("", error, $"Failed to find TaxCode in MYOB with code '{settings.DefaultTaxCode}'");
  180. return Result.Error(new Exception($"Failed to find TaxCode in MYOB with code '{settings.DefaultTaxCode}': {error.Message}", error));
  181. }
  182. }
  183. return Result.Ok();
  184. }
  185. /// <summary>
  186. /// Try to find a customer in MYOB which matches <paramref name="customer"/>, and if this fails, create a new one.
  187. /// </summary>
  188. /// <remarks>
  189. /// After this has finished, <paramref name="customer"/> will be updated with <see cref="Customer.PostedReference"/> set to the correct ID.
  190. /// <br/>
  191. /// <paramref name="customer"/> needs to have at least <see cref="Customer.Code"/> and <see cref="Customer.PostedReference"/> as loaded columns.
  192. /// </remarks>
  193. /// <param name="data"></param>
  194. /// <param name="customer">The customer to map to.</param>
  195. /// <returns>The UID of the MYOB customer.</returns>
  196. public static Result<Guid, Exception> MapCustomer(MYOBConnectionData data, Customer customer)
  197. {
  198. if(Guid.TryParse(customer.PostedReference, out var myobID))
  199. {
  200. return new Result<Guid, Exception>(myobID);
  201. }
  202. var service = new CustomerService(data.Configuration, null, data.AuthKey);
  203. var result = service.Query(data, new Filter<MYOBCustomer>(x => x.DisplayID).IsEqualTo(customer.Code), top: 1);
  204. return result.MapOk(customers =>
  205. {
  206. if(customers.Count == 0)
  207. {
  208. if(customer.Code.Length > 15)
  209. {
  210. return Result.Error(new Exception("Customer code is longer than 15 characters"));
  211. }
  212. var myobCustomer = new MYOBCustomer();
  213. return UpdateCustomer(data, PosterUtils.LoadPosterSettings<Customer, CustomerMYOBPosterSettings>(), customer, myobCustomer, true)
  214. .MapOk<Result<Guid, Exception>>(() =>
  215. {
  216. try
  217. {
  218. var result = service.UpdateEx(data.CompanyFile, myobCustomer, data.CompanyFileCredentials);
  219. customer.PostedReference = result.UID.ToString();
  220. return Result.Ok(result.UID);
  221. }
  222. catch(ApiCommunicationException e)
  223. {
  224. return Result.Error(new Exception(PRSMYOBPosterUtils.FormatApiException(e), e));
  225. }
  226. catch (Exception e)
  227. {
  228. CoreUtils.LogException("", e, $"Error while posting customer {customer.ID}");
  229. return Result.Error(e);
  230. }
  231. }).Flatten();
  232. }
  233. else
  234. {
  235. customer.PostedReference = customers.Items[0].UID.ToString();
  236. return Result.Ok(customers.Items[0].UID);
  237. }
  238. }).Flatten();
  239. }
  240. public IPostResult<Customer> Process(IDataModel<Customer> model)
  241. {
  242. var results = new PostResult<Customer>();
  243. var service = new CustomerService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
  244. var customers = model.GetTable<Customer>().ToArray<Customer>();
  245. foreach(var customer in customers)
  246. {
  247. if(customer.Code.Length > 15)
  248. {
  249. results.AddFailed(customer, "Code is longer than 15 characters.");
  250. continue;
  251. }
  252. bool isNew;
  253. MYOBCustomer myobCustomer;
  254. Exception? error;
  255. if(Guid.TryParse(customer.PostedReference, out var myobID))
  256. {
  257. if(!service.Get(ConnectionData, myobID).Get(out var newCustomer, out error))
  258. {
  259. CoreUtils.LogException("", error, $"Failed to find Customer in MYOB with id {myobID}");
  260. results.AddFailed(customer, $"Failed to find Customer in MYOB with id {myobID}: {error.Message}");
  261. continue;
  262. }
  263. myobCustomer = newCustomer;
  264. isNew = false;
  265. }
  266. else
  267. {
  268. if(service.Query(ConnectionData, new Filter<MYOBCustomer>(x => x.DisplayID).IsEqualTo(customer.Code)).Get(out var myobCustomers, out error))
  269. {
  270. if(myobCustomers.Items.Length > 0)
  271. {
  272. myobCustomer = myobCustomers.Items[0];
  273. isNew = false;
  274. }
  275. else
  276. {
  277. myobCustomer = new MYOBCustomer();
  278. isNew = true;
  279. }
  280. }
  281. else
  282. {
  283. CoreUtils.LogException("", error);
  284. results.AddFailed(customer, error.Message);
  285. continue;
  286. }
  287. }
  288. if(UpdateCustomer(ConnectionData, Settings, customer, myobCustomer, isNew).Get(out error))
  289. {
  290. try
  291. {
  292. var result = service.Save(ConnectionData, myobCustomer);
  293. customer.PostedReference = result.UID.ToString();
  294. results.AddSuccess(customer);
  295. }
  296. catch(ApiCommunicationException e)
  297. {
  298. results.AddFailed(customer, PRSMYOBPosterUtils.FormatApiException(e));
  299. }
  300. catch(Exception e)
  301. {
  302. CoreUtils.LogException("", e, $"Error while posting customer {customer.ID}");
  303. results.AddFailed(customer, e.Message);
  304. }
  305. }
  306. else
  307. {
  308. results.AddFailed(customer, error.Message);
  309. }
  310. }
  311. return results;
  312. }
  313. }
  314. public class CustomerMYOBPosterEngine<T> : MYOBPosterEngine<Customer, CustomerMYOBPosterSettings> { }