CustomerMYOBPoster.cs 11 KB

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