SupplierMYOBPoster.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.Core.Postable;
  5. using InABox.Database;
  6. using InABox.Poster.MYOB;
  7. using InABox.Scripting;
  8. using MYOB.AccountRight.SDK.Services.Contact;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Linq;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. using MYOBSupplier = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Supplier;
  15. namespace PRS.Shared.Posters.MYOB;
  16. public class SupplierMYOBPosterSettings : MYOBPosterSettings
  17. {
  18. public override string DefaultScript(Type TPostable)
  19. {
  20. return @"using MYOBSupplier = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Supplier;
  21. public class Module
  22. {
  23. public void BeforePost(IDataModel<Supplier> model)
  24. {
  25. // Perform pre-processing
  26. }
  27. public void ProcessSupplier(IDataModel<Supplier> model, Supplier supplier, MYOBSupplier myobSupplier)
  28. {
  29. // Do extra processing for a supplier; throw an exception to fail this supplier.
  30. }
  31. }";
  32. }
  33. }
  34. public class SupplierMYOBAutoRefresher : IAutoRefresher<Supplier>
  35. {
  36. public bool ShouldRepost(Supplier supplier)
  37. {
  38. var shouldRepost = supplier.HasOriginalValue(x => x.Name)
  39. || supplier.HasOriginalValue(x => x.Code)
  40. || supplier.HasOriginalValue(x => x.ABN)
  41. || supplier.HasOriginalValue(x => x.Email)
  42. || supplier.HasOriginalValue(x => x.Telephone)
  43. || supplier.Delivery.HasOriginalValue(x => x.Street)
  44. || supplier.Delivery.HasOriginalValue(x => x.City)
  45. || supplier.Delivery.HasOriginalValue(x => x.State)
  46. || supplier.Delivery.HasOriginalValue(x => x.PostCode)
  47. || supplier.Postal.HasOriginalValue(x => x.Street)
  48. || supplier.Postal.HasOriginalValue(x => x.City)
  49. || supplier.Postal.HasOriginalValue(x => x.State)
  50. || supplier.Postal.HasOriginalValue(x => x.PostCode);
  51. if (shouldRepost)
  52. {
  53. return true;
  54. }
  55. if(supplier.SupplierStatus.HasOriginalValue(x => x.ID))
  56. {
  57. var originalID = supplier.SupplierStatus.GetOriginalValue(x => x.ID);
  58. var currentID = supplier.SupplierStatus.ID;
  59. var statuses = DbFactory.NewProvider(Logger.Main).Query(
  60. new Filter<SupplierStatus>(x => x.ID).IsEqualTo(originalID).Or(x => x.ID).IsEqualTo(currentID),
  61. Columns.None<SupplierStatus>().Add(x => x.ID).Add(x => x.Active))
  62. .ToArray<SupplierStatus>();
  63. if (statuses.Length == 2 && statuses[0].Active != statuses[1].Active)
  64. {
  65. return true;
  66. }
  67. }
  68. return false;
  69. }
  70. }
  71. public class SupplierMYOBPoster : IMYOBPoster<Supplier, SupplierMYOBPosterSettings>, IAutoRefreshPoster<Supplier, SupplierMYOBAutoRefresher>
  72. {
  73. public ScriptDocument? Script { get; set; }
  74. public MYOBConnectionData ConnectionData { get; set; }
  75. public SupplierMYOBPosterSettings Settings { get; set; }
  76. public MYOBGlobalPosterSettings GlobalSettings { get; set; }
  77. public bool BeforePost(IDataModel<Supplier> model)
  78. {
  79. foreach (var (_, table) in model.ModelTables)
  80. {
  81. table.IsDefault = false;
  82. }
  83. model.SetIsDefault<Supplier>(true);
  84. model.SetColumns<Supplier>(RequiredColumns());
  85. Script?.Execute(methodname: "BeforePost", parameters: [model]);
  86. return true;
  87. }
  88. #region Script Functions
  89. private Result<Exception> ProcessSupplier(IDataModel<Supplier> model, Supplier supplier, MYOBSupplier myobSupplier)
  90. {
  91. return this.WrapScript("ProcessSupplier", model, supplier, myobSupplier);
  92. }
  93. #endregion
  94. public static Columns<Supplier> RequiredColumns()
  95. {
  96. return Columns.None<Supplier>()
  97. .Add(x => x.ID)
  98. .Add(x => x.PostedReference)
  99. .Add(x => x.PostedStatus)
  100. .Add(x => x.Name)
  101. .Add(x => x.Code)
  102. .Add(x => x.SupplierStatus.ID)
  103. .Add(x => x.SupplierStatus.Active)
  104. .Add(x => x.Postal.Street)
  105. .Add(x => x.Postal.City)
  106. .Add(x => x.Postal.State)
  107. .Add(x => x.Postal.PostCode)
  108. .Add(x => x.Delivery.Street)
  109. .Add(x => x.Delivery.City)
  110. .Add(x => x.Delivery.State)
  111. .Add(x => x.Delivery.PostCode)
  112. .Add(x => x.Email)
  113. .Add(x => x.Telephone)
  114. .Add(x => x.ABN);
  115. }
  116. public static Result<Exception> UpdateSupplier(MYOBConnectionData data, MYOBGlobalPosterSettings settings, Supplier supplier, MYOBSupplier myobSupplier, bool isNew)
  117. {
  118. // Documentation: https://developer.myob.com/api/myob-business-api/v2/contact/supplier/
  119. // Since this might be called from some other poster, we need to ensure we have the right columns.
  120. Client.EnsureColumns(supplier, RequiredColumns());
  121. // ContactMYOBUtils.SplitName(supplier.DefaultContact.Name, out var firstName, out var lastName);
  122. myobSupplier.CompanyName = supplier.Name.Truncate(50);
  123. // myobSupplier.FirstName =
  124. // myobSupplier.LastName =
  125. myobSupplier.IsIndividual = false;
  126. myobSupplier.DisplayID = supplier.Code.Truncate(15);
  127. // If there is not customer status, we will use default to Active = true.
  128. myobSupplier.IsActive = supplier.SupplierStatus.ID == Guid.Empty || supplier.SupplierStatus.Active;
  129. myobSupplier.Addresses =
  130. [
  131. ContactMYOBUtils.ConvertAddress(supplier.Postal, 1, new Contact
  132. {
  133. Email = supplier.Email,
  134. Telephone = supplier.Telephone
  135. }),
  136. ContactMYOBUtils.ConvertAddress(supplier.Delivery, 2, new Contact
  137. {
  138. Email = supplier.Email,
  139. Telephone = supplier.Telephone
  140. })
  141. ];
  142. // Notes =
  143. // PhotoURI =
  144. // RowVersion =
  145. myobSupplier.BuyingDetails ??= new();
  146. // myobSupplier.BuyingDetails.PurchaseLayout =
  147. // myobCustomer.BuyingDetails.PrintedForm =
  148. // myobSupplier.BuyingDetails.PurchaseOrderDelivery =
  149. // myobCustomer.BuyingDetails.ExpenseAccount.UID =
  150. // myobCustomer.BuyingDetails.PaymentMemo =
  151. // myobCustomer.BuyingDetails.PurchaseComment =
  152. // myobCustomer.BuyingDetails.SupplierBillingRate =
  153. // myobCustomer.BuyingDetails.ShippingMethod =
  154. // myobCustomer.BuyingDetails.IsReportable =
  155. // myobCustomer.BuyingDetails.CostPerHour =
  156. // myobCustomer.BuyingDetails.Credit.Limit =
  157. myobSupplier.BuyingDetails.ABN = supplier.ABN.Truncate(14);
  158. // myobCustomer.BuyingDetails.ABNBranch
  159. // myobCustomer.BuyingDetails.TaxIdNumber
  160. if (isNew)
  161. {
  162. if(!MYOBPosterUtils.GetDefaultTaxCode(data, settings).Get(out var taxID, out var error))
  163. {
  164. return Result.Error(error);
  165. }
  166. myobSupplier.BuyingDetails.TaxCode ??= new();
  167. myobSupplier.BuyingDetails.TaxCode.UID = taxID;
  168. myobSupplier.BuyingDetails.FreightTaxCode ??= new();
  169. myobSupplier.BuyingDetails.FreightTaxCode.UID = taxID;
  170. }
  171. // myobCustomer.BuyingDetails.UseSupplierTaxCode
  172. // myobCustomer.BuyingDetails.Terms
  173. // myobCustomer.PaymentDetails
  174. // myobCustomer.PhotoURI
  175. return Result.Ok();
  176. }
  177. /// <summary>
  178. /// Try to find a supplier in MYOB which matches <paramref name="supplier"/>, and if this fails, create a new one.
  179. /// </summary>
  180. /// <remarks>
  181. /// After this has finished, <paramref name="supplier"/> will be updated with <see cref="Supplier.PostedReference"/> set to the correct ID.
  182. /// <br/>
  183. /// <paramref name="supplier"/> needs to have at least <see cref="Supplier.Code"/> and <see cref="Supplier.PostedReference"/> as loaded columns.
  184. /// </remarks>
  185. /// <param name="data"></param>
  186. /// <param name="supplier">The supplier to map to.</param>
  187. /// <returns>The UID of the MYOB supplier.</returns>
  188. public static Result<Guid, Exception> MapSupplier(MYOBConnectionData data, Supplier supplier, MYOBGlobalPosterSettings settings)
  189. {
  190. if(Guid.TryParse(supplier.PostedReference, out var myobID))
  191. {
  192. return Result.Ok(myobID);
  193. }
  194. var service = new SupplierService(data.Configuration, null, data.AuthKey);
  195. var result = service.Query(data, new Filter<MYOBSupplier>(x => x.DisplayID).IsEqualTo(supplier.Code), top: 1);
  196. return result.MapOk(suppliers =>
  197. {
  198. if(suppliers.Items.Length == 0)
  199. {
  200. if(supplier.Code.Length > 15)
  201. {
  202. return Result.Error(new Exception("Customer code is longer than 15 characters"));
  203. }
  204. var myobSupplier = new MYOBSupplier();
  205. return UpdateSupplier(data, settings, supplier, myobSupplier, true)
  206. .MapOk(() => service.Save(data, myobSupplier)
  207. .MapOk(x =>
  208. {
  209. supplier.PostedReference = x.UID.ToString();
  210. // Marking as repost because a script may not have run.
  211. supplier.PostedStatus = PostedStatus.RequiresRepost;
  212. return x.UID;
  213. })).Flatten();
  214. }
  215. else
  216. {
  217. supplier.PostedReference = suppliers.Items[0].UID.ToString();
  218. supplier.PostedStatus = PostedStatus.RequiresRepost;
  219. return Result.Ok(suppliers.Items[0].UID);
  220. }
  221. }).Flatten();
  222. }
  223. private static bool IsBlankCode(string code)
  224. {
  225. return code.IsNullOrWhiteSpace() || code.Equals("*None");
  226. }
  227. public IPullResult<Supplier> Pull()
  228. {
  229. var result = new PullResult<Supplier>();
  230. var top = 400;
  231. var skip = 0;
  232. var supplierCodes = new HashSet<string>();
  233. var service = new SupplierService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
  234. while (true)
  235. {
  236. if(!service.Query(ConnectionData, null, top: top, skip: skip).Get(out var myobSuppliers, out var error))
  237. {
  238. CoreUtils.LogException("", error);
  239. throw new PullFailedMessageException(error.Message);
  240. }
  241. if(myobSuppliers.Items.Length == 0)
  242. {
  243. break;
  244. }
  245. var myobIDs = myobSuppliers.Items.ToArray(x => x.UID.ToString());
  246. var myobCodes = myobSuppliers.Items.Select(x => x.DisplayID).Where(x => !IsBlankCode(x)).ToArray();
  247. var myobNames = myobSuppliers.Items.Where(x => IsBlankCode(x.DisplayID) && !x.CompanyName.IsNullOrWhiteSpace())
  248. .Select(x => x.CompanyName).ToArray();
  249. var suppliers = Client.Query(
  250. new Filter<Supplier>(x => x.PostedReference).InList(myobIDs)
  251. .Or(x => x.Code).InList(myobCodes)
  252. .Or(x => x.Name).InList(myobNames),
  253. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.PostedReference).Add(x => x.Code).Add(x => x.Name))
  254. .ToArray<Supplier>();
  255. var supplierDict = suppliers.Where(x => !x.PostedReference.IsNullOrWhiteSpace())
  256. .ToDictionary(x => x.PostedReference);
  257. var blankSuppliers = suppliers.Where(x => x.PostedReference.IsNullOrWhiteSpace()).ToArray();
  258. var needCodes = new Dictionary<string, (string prefix, int i, Supplier supplier)>();
  259. foreach(var myobSupplier in myobSuppliers.Items)
  260. {
  261. if (supplierDict.TryGetValue(myobSupplier.UID.ToString(), out var supplier))
  262. {
  263. // Skipping existing suppliers at this point.
  264. continue;
  265. }
  266. supplier = !IsBlankCode(myobSupplier.DisplayID)
  267. ? blankSuppliers.FirstOrDefault(x => string.Equals(x.Code, myobSupplier.DisplayID))
  268. : blankSuppliers.FirstOrDefault(x => string.Equals(x.Name, myobSupplier.CompanyName));
  269. if(supplier is not null)
  270. {
  271. supplier.PostedReference = myobSupplier.UID.ToString();
  272. result.AddEntity(PullResultType.Linked, supplier);
  273. continue;
  274. }
  275. supplier = new Supplier();
  276. string code;
  277. if (!IsBlankCode(myobSupplier.DisplayID))
  278. {
  279. code = myobSupplier.DisplayID.ToString();
  280. }
  281. else if (!myobSupplier.CompanyName.IsNullOrWhiteSpace())
  282. {
  283. code = myobSupplier.CompanyName[..Math.Min(3, myobSupplier.CompanyName.Length)].ToUpper();
  284. }
  285. else
  286. {
  287. code = "SUP";
  288. }
  289. int i = 1;
  290. supplier.Code = code;
  291. while (supplierCodes.Contains(supplier.Code))
  292. {
  293. supplier.Code = $"{code}{i:d3}";
  294. ++i;
  295. }
  296. supplierCodes.Add(supplier.Code);
  297. supplier.Name = myobSupplier.CompanyName;
  298. supplier.ABN = myobSupplier.BuyingDetails.ABN;
  299. if(myobSupplier.Addresses is not null)
  300. {
  301. var delivery = myobSupplier.Addresses.FirstOrDefault(x => x.Location == 2);
  302. if(delivery is not null)
  303. {
  304. supplier.Delivery.CopyFrom(ContactMYOBUtils.ConvertAddress(delivery));
  305. }
  306. var postal = myobSupplier.Addresses.FirstOrDefault(x => x.Location == 1);
  307. if(postal is not null)
  308. {
  309. supplier.Postal.CopyFrom(ContactMYOBUtils.ConvertAddress(postal));
  310. }
  311. supplier.Email = delivery?.Email ?? postal?.Email ?? "";
  312. }
  313. supplier.PostedReference = myobSupplier.UID.ToString();
  314. result.AddEntity(PullResultType.New, supplier);
  315. needCodes.Add(supplier.Code, (code, i, supplier));
  316. }
  317. // Do code clash checking
  318. while(needCodes.Count > 0)
  319. {
  320. var codes = Client.Query(
  321. new Filter<Supplier>(x => x.Code).InList(needCodes.Values.Select(x => x.supplier.Code).ToArray()),
  322. Columns.None<Supplier>().Add(x => x.Code));
  323. var newNeedCodes = new Dictionary<string, (string prefix, int i, Supplier supplier)>();
  324. foreach(var row in codes.Rows)
  325. {
  326. var code = row.Get<Supplier, string>(x => x.Code);
  327. if(needCodes.Remove(code, out var needed))
  328. {
  329. int i = needed.i;
  330. do
  331. {
  332. needed.supplier.Code = $"{needed.prefix}{i:d3}";
  333. ++i;
  334. } while (supplierCodes.Contains(needed.supplier.Code));
  335. supplierCodes.Add(needed.supplier.Code);
  336. newNeedCodes.Add(needed.supplier.Code, (needed.prefix, i, needed.supplier));
  337. }
  338. }
  339. needCodes = newNeedCodes;
  340. }
  341. skip += top;
  342. }
  343. return result;
  344. }
  345. public IPostResult<Supplier> Process(IDataModel<Supplier> model)
  346. {
  347. var results = new PostResult<Supplier>();
  348. var service = new SupplierService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
  349. var suppliers = model.GetTable<Supplier>().ToArray<Supplier>();
  350. foreach(var supplier in suppliers)
  351. {
  352. if(supplier.Code.Length > 15)
  353. {
  354. results.AddFailed(supplier, "Code is longer than 15 characters.");
  355. continue;
  356. }
  357. bool isNew;
  358. MYOBSupplier myobSupplier;
  359. Exception? error;
  360. if(Guid.TryParse(supplier.PostedReference, out var myobID))
  361. {
  362. if(!service.Get(ConnectionData, myobID).Get(out var newSupplier, out error))
  363. {
  364. CoreUtils.LogException("", error, $"Failed to find Supplier in MYOB with id {myobID}");
  365. results.AddFailed(supplier, $"Failed to find Supplier in MYOB with id {myobID}: {error.Message}");
  366. continue;
  367. }
  368. myobSupplier = newSupplier;
  369. isNew = false;
  370. }
  371. else
  372. {
  373. if(service.Query(
  374. ConnectionData,
  375. new Filter<MYOBSupplier>(x => x.DisplayID).IsEqualTo(supplier.Code),
  376. top: 1).Get(out var myobSuppliers, out error))
  377. {
  378. if(myobSuppliers.Items.Length > 0)
  379. {
  380. myobSupplier = myobSuppliers.Items[0];
  381. isNew = false;
  382. }
  383. else if(service.Query(
  384. ConnectionData,
  385. new Filter<MYOBSupplier>(x => x.CompanyName).IsEqualTo(supplier.Name)
  386. .And(new Filter<MYOBSupplier>(x => x.DisplayID).IsEqualTo(null)
  387. .Or(x => x.DisplayID).IsEqualTo("")
  388. .Or(x => x.DisplayID).IsEqualTo("*None")),
  389. top: 1).Get(out myobSuppliers, out error))
  390. {
  391. if(myobSuppliers.Items.Length > 0)
  392. {
  393. myobSupplier = myobSuppliers.Items[0];
  394. myobSupplier.DisplayID = supplier.Code;
  395. isNew = false;
  396. }
  397. else
  398. {
  399. myobSupplier = new MYOBSupplier();
  400. isNew = true;
  401. }
  402. }
  403. else
  404. {
  405. CoreUtils.LogException("", error);
  406. results.AddFailed(supplier, error.Message);
  407. continue;
  408. }
  409. }
  410. else
  411. {
  412. CoreUtils.LogException("", error);
  413. results.AddFailed(supplier, error.Message);
  414. continue;
  415. }
  416. }
  417. if(UpdateSupplier(ConnectionData, GlobalSettings, supplier, myobSupplier, isNew)
  418. .MapOk(() => ProcessSupplier(model, supplier, myobSupplier)).Flatten()
  419. .MapOk(() => service.Save(ConnectionData, myobSupplier)).Flatten()
  420. .Get(out var result, out error))
  421. {
  422. supplier.PostedReference = result.UID.ToString();
  423. results.AddSuccess(supplier);
  424. }
  425. else
  426. {
  427. CoreUtils.LogException("", error, $"Error while posting supplier {supplier.ID}");
  428. results.AddFailed(supplier, error.Message);
  429. }
  430. }
  431. return results;
  432. }
  433. }
  434. public class SupplierMYOBPosterEngine<T> : MYOBPosterEngine<Supplier, SupplierMYOBPoster, SupplierMYOBPosterSettings>, IPullerEngine<Supplier, SupplierMYOBPoster>
  435. {
  436. public IPullResult<Supplier> DoPull()
  437. {
  438. LoadConnectionData();
  439. return Poster.Pull();
  440. }
  441. }