using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.Core.Postable; using InABox.Database; using InABox.Poster.MYOB; using InABox.Scripting; using MYOB.AccountRight.SDK.Services.Contact; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MYOBSupplier = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Supplier; namespace PRS.Shared.Posters.MYOB; public class SupplierMYOBPosterSettings : MYOBPosterSettings { public override string DefaultScript(Type TPostable) { return @"using MYOBSupplier = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Supplier; public class Module { public void BeforePost(IDataModel model) { // Perform pre-processing } public void ProcessSupplier(IDataModel model, Supplier supplier, MYOBSupplier myobSupplier) { // Do extra processing for a supplier; throw an exception to fail this supplier. } }"; } } public class SupplierMYOBAutoRefresher : IAutoRefresher { public bool ShouldRepost(Supplier supplier) { var shouldRepost = supplier.HasOriginalValue(x => x.Name) || supplier.HasOriginalValue(x => x.Code) || supplier.HasOriginalValue(x => x.ABN) || supplier.HasOriginalValue(x => x.Email) || supplier.HasOriginalValue(x => x.Telephone) || supplier.Delivery.HasOriginalValue(x => x.Street) || supplier.Delivery.HasOriginalValue(x => x.City) || supplier.Delivery.HasOriginalValue(x => x.State) || supplier.Delivery.HasOriginalValue(x => x.PostCode) || supplier.Postal.HasOriginalValue(x => x.Street) || supplier.Postal.HasOriginalValue(x => x.City) || supplier.Postal.HasOriginalValue(x => x.State) || supplier.Postal.HasOriginalValue(x => x.PostCode); if (shouldRepost) { return true; } if(supplier.SupplierStatus.HasOriginalValue(x => x.ID)) { var originalID = supplier.SupplierStatus.GetOriginalValue(x => x.ID); var currentID = supplier.SupplierStatus.ID; var statuses = DbFactory.NewProvider(Logger.Main).Query( new Filter(x => x.ID).IsEqualTo(originalID).Or(x => x.ID).IsEqualTo(currentID), Columns.None().Add(x => x.ID).Add(x => x.Active)) .ToArray(); if (statuses.Length == 2 && statuses[0].Active != statuses[1].Active) { return true; } } return false; } } public class SupplierMYOBPoster : IMYOBPoster, IAutoRefreshPoster { public ScriptDocument? Script { get; set; } public MYOBConnectionData ConnectionData { get; set; } public SupplierMYOBPosterSettings Settings { get; set; } public MYOBGlobalPosterSettings GlobalSettings { get; set; } public bool BeforePost(IDataModel model) { foreach (var (_, table) in model.ModelTables) { table.IsDefault = false; } model.SetIsDefault(true); model.SetColumns(RequiredColumns()); Script?.Execute(methodname: "BeforePost", parameters: [model]); return true; } #region Script Functions private Result ProcessSupplier(IDataModel model, Supplier supplier, MYOBSupplier myobSupplier) { return this.WrapScript("ProcessSupplier", model, supplier, myobSupplier); } #endregion public static Columns RequiredColumns() { return Columns.None() .Add(x => x.ID) .Add(x => x.PostedReference) .Add(x => x.PostedStatus) .Add(x => x.Name) .Add(x => x.Code) .Add(x => x.SupplierStatus.ID) .Add(x => x.SupplierStatus.Active) .Add(x => x.Postal.Street) .Add(x => x.Postal.City) .Add(x => x.Postal.State) .Add(x => x.Postal.PostCode) .Add(x => x.Delivery.Street) .Add(x => x.Delivery.City) .Add(x => x.Delivery.State) .Add(x => x.Delivery.PostCode) .Add(x => x.Email) .Add(x => x.Telephone) .Add(x => x.ABN); } public static Result UpdateSupplier(MYOBConnectionData data, MYOBGlobalPosterSettings settings, Supplier supplier, MYOBSupplier myobSupplier, bool isNew) { // Documentation: https://developer.myob.com/api/myob-business-api/v2/contact/supplier/ // Since this might be called from some other poster, we need to ensure we have the right columns. Client.EnsureColumns(supplier, RequiredColumns()); // ContactMYOBUtils.SplitName(supplier.DefaultContact.Name, out var firstName, out var lastName); myobSupplier.CompanyName = supplier.Name.Truncate(50); // myobSupplier.FirstName = // myobSupplier.LastName = myobSupplier.IsIndividual = false; myobSupplier.DisplayID = supplier.Code.Truncate(15); // If there is not customer status, we will use default to Active = true. myobSupplier.IsActive = supplier.SupplierStatus.ID == Guid.Empty || supplier.SupplierStatus.Active; myobSupplier.Addresses = [ ContactMYOBUtils.ConvertAddress(supplier.Postal, 1, new Contact { Email = supplier.Email, Telephone = supplier.Telephone }), ContactMYOBUtils.ConvertAddress(supplier.Delivery, 2, new Contact { Email = supplier.Email, Telephone = supplier.Telephone }) ]; // Notes = // PhotoURI = // RowVersion = myobSupplier.BuyingDetails ??= new(); // myobSupplier.BuyingDetails.PurchaseLayout = // myobCustomer.BuyingDetails.PrintedForm = // myobSupplier.BuyingDetails.PurchaseOrderDelivery = // myobCustomer.BuyingDetails.ExpenseAccount.UID = // myobCustomer.BuyingDetails.PaymentMemo = // myobCustomer.BuyingDetails.PurchaseComment = // myobCustomer.BuyingDetails.SupplierBillingRate = // myobCustomer.BuyingDetails.ShippingMethod = // myobCustomer.BuyingDetails.IsReportable = // myobCustomer.BuyingDetails.CostPerHour = // myobCustomer.BuyingDetails.Credit.Limit = myobSupplier.BuyingDetails.ABN = supplier.ABN.Truncate(14); // myobCustomer.BuyingDetails.ABNBranch // myobCustomer.BuyingDetails.TaxIdNumber if (isNew) { if(!MYOBPosterUtils.GetDefaultTaxCode(data, settings).Get(out var taxID, out var error)) { return Result.Error(error); } myobSupplier.BuyingDetails.TaxCode ??= new(); myobSupplier.BuyingDetails.TaxCode.UID = taxID; myobSupplier.BuyingDetails.FreightTaxCode ??= new(); myobSupplier.BuyingDetails.FreightTaxCode.UID = taxID; } // myobCustomer.BuyingDetails.UseSupplierTaxCode // myobCustomer.BuyingDetails.Terms // myobCustomer.PaymentDetails // myobCustomer.PhotoURI return Result.Ok(); } /// /// Try to find a supplier in MYOB which matches , and if this fails, create a new one. /// /// /// After this has finished, will be updated with set to the correct ID. ///
/// needs to have at least and as loaded columns. ///
/// /// The supplier to map to. /// The UID of the MYOB supplier. public static Result MapSupplier(MYOBConnectionData data, Supplier supplier, MYOBGlobalPosterSettings settings) { if(Guid.TryParse(supplier.PostedReference, out var myobID)) { return Result.Ok(myobID); } var service = new SupplierService(data.Configuration, null, data.AuthKey); var result = service.Query(data, new Filter(x => x.DisplayID).IsEqualTo(supplier.Code), top: 1); return result.MapOk(suppliers => { if(suppliers.Items.Length == 0) { if(supplier.Code.Length > 15) { return Result.Error(new Exception("Customer code is longer than 15 characters")); } var myobSupplier = new MYOBSupplier(); return UpdateSupplier(data, settings, supplier, myobSupplier, true) .MapOk(() => service.Save(data, myobSupplier) .MapOk(x => { supplier.PostedReference = x.UID.ToString(); // Marking as repost because a script may not have run. supplier.PostedStatus = PostedStatus.RequiresRepost; return x.UID; })).Flatten(); } else { supplier.PostedReference = suppliers.Items[0].UID.ToString(); supplier.PostedStatus = PostedStatus.RequiresRepost; return Result.Ok(suppliers.Items[0].UID); } }).Flatten(); } private static bool IsBlankCode(string code) { return code.IsNullOrWhiteSpace() || code.Equals("*None"); } public IPullResult Pull() { var result = new PullResult(); var top = 400; var skip = 0; var supplierCodes = new HashSet(); var service = new SupplierService(ConnectionData.Configuration, null, ConnectionData.AuthKey); while (true) { if(!service.Query(ConnectionData, null, top: top, skip: skip).Get(out var myobSuppliers, out var error)) { CoreUtils.LogException("", error); throw new PullFailedMessageException(error.Message); } if(myobSuppliers.Items.Length == 0) { break; } var myobIDs = myobSuppliers.Items.ToArray(x => x.UID.ToString()); var myobCodes = myobSuppliers.Items.Select(x => x.DisplayID).Where(x => !IsBlankCode(x)).ToArray(); var myobNames = myobSuppliers.Items.Where(x => IsBlankCode(x.DisplayID) && !x.CompanyName.IsNullOrWhiteSpace()) .Select(x => x.CompanyName).ToArray(); var suppliers = Client.Query( new Filter(x => x.PostedReference).InList(myobIDs) .Or(x => x.Code).InList(myobCodes) .Or(x => x.Name).InList(myobNames), Columns.None().Add(x => x.ID).Add(x => x.PostedReference).Add(x => x.Code).Add(x => x.Name)) .ToArray(); var supplierDict = suppliers.Where(x => !x.PostedReference.IsNullOrWhiteSpace()) .ToDictionary(x => x.PostedReference); var blankSuppliers = suppliers.Where(x => x.PostedReference.IsNullOrWhiteSpace()).ToArray(); var needCodes = new Dictionary(); foreach(var myobSupplier in myobSuppliers.Items) { if (supplierDict.TryGetValue(myobSupplier.UID.ToString(), out var supplier)) { // Skipping existing suppliers at this point. continue; } supplier = !IsBlankCode(myobSupplier.DisplayID) ? blankSuppliers.FirstOrDefault(x => string.Equals(x.Code, myobSupplier.DisplayID)) : blankSuppliers.FirstOrDefault(x => string.Equals(x.Name, myobSupplier.CompanyName)); if(supplier is not null) { supplier.PostedReference = myobSupplier.UID.ToString(); result.AddEntity(PullResultType.Linked, supplier); continue; } supplier = new Supplier(); string code; if (!IsBlankCode(myobSupplier.DisplayID)) { code = myobSupplier.DisplayID.ToString(); } else if (!myobSupplier.CompanyName.IsNullOrWhiteSpace()) { code = myobSupplier.CompanyName[..Math.Min(3, myobSupplier.CompanyName.Length)].ToUpper(); } else { code = "SUP"; } int i = 1; supplier.Code = code; while (supplierCodes.Contains(supplier.Code)) { supplier.Code = $"{code}{i:d3}"; ++i; } supplierCodes.Add(supplier.Code); supplier.Name = myobSupplier.CompanyName; supplier.ABN = myobSupplier.BuyingDetails.ABN; if(myobSupplier.Addresses is not null) { var delivery = myobSupplier.Addresses.FirstOrDefault(x => x.Location == 2); if(delivery is not null) { supplier.Delivery.CopyFrom(ContactMYOBUtils.ConvertAddress(delivery)); } var postal = myobSupplier.Addresses.FirstOrDefault(x => x.Location == 1); if(postal is not null) { supplier.Postal.CopyFrom(ContactMYOBUtils.ConvertAddress(postal)); } supplier.Email = delivery?.Email ?? postal?.Email ?? ""; } supplier.PostedReference = myobSupplier.UID.ToString(); result.AddEntity(PullResultType.New, supplier); needCodes.Add(supplier.Code, (code, i, supplier)); } // Do code clash checking while(needCodes.Count > 0) { var codes = Client.Query( new Filter(x => x.Code).InList(needCodes.Values.Select(x => x.supplier.Code).ToArray()), Columns.None().Add(x => x.Code)); var newNeedCodes = new Dictionary(); foreach(var row in codes.Rows) { var code = row.Get(x => x.Code); if(needCodes.Remove(code, out var needed)) { int i = needed.i; do { needed.supplier.Code = $"{needed.prefix}{i:d3}"; ++i; } while (supplierCodes.Contains(needed.supplier.Code)); supplierCodes.Add(needed.supplier.Code); newNeedCodes.Add(needed.supplier.Code, (needed.prefix, i, needed.supplier)); } } needCodes = newNeedCodes; } skip += top; } return result; } public IPostResult Process(IDataModel model) { var results = new PostResult(); var service = new SupplierService(ConnectionData.Configuration, null, ConnectionData.AuthKey); var suppliers = model.GetTable().ToArray(); foreach(var supplier in suppliers) { if(supplier.Code.Length > 15) { results.AddFailed(supplier, "Code is longer than 15 characters."); continue; } bool isNew; MYOBSupplier myobSupplier; Exception? error; if(Guid.TryParse(supplier.PostedReference, out var myobID)) { if(!service.Get(ConnectionData, myobID).Get(out var newSupplier, out error)) { CoreUtils.LogException("", error, $"Failed to find Supplier in MYOB with id {myobID}"); results.AddFailed(supplier, $"Failed to find Supplier in MYOB with id {myobID}: {error.Message}"); continue; } myobSupplier = newSupplier; isNew = false; } else { if(service.Query( ConnectionData, new Filter(x => x.DisplayID).IsEqualTo(supplier.Code), top: 1).Get(out var myobSuppliers, out error)) { if(myobSuppliers.Items.Length > 0) { myobSupplier = myobSuppliers.Items[0]; isNew = false; } else if(service.Query( ConnectionData, new Filter(x => x.CompanyName).IsEqualTo(supplier.Name) .And(new Filter(x => x.DisplayID).IsEqualTo(null) .Or(x => x.DisplayID).IsEqualTo("") .Or(x => x.DisplayID).IsEqualTo("*None")), top: 1).Get(out myobSuppliers, out error)) { if(myobSuppliers.Items.Length > 0) { myobSupplier = myobSuppliers.Items[0]; myobSupplier.DisplayID = supplier.Code; isNew = false; } else { myobSupplier = new MYOBSupplier(); isNew = true; } } else { CoreUtils.LogException("", error); results.AddFailed(supplier, error.Message); continue; } } else { CoreUtils.LogException("", error); results.AddFailed(supplier, error.Message); continue; } } if(UpdateSupplier(ConnectionData, GlobalSettings, supplier, myobSupplier, isNew) .MapOk(() => ProcessSupplier(model, supplier, myobSupplier)).Flatten() .MapOk(() => service.Save(ConnectionData, myobSupplier)).Flatten() .Get(out var result, out error)) { supplier.PostedReference = result.UID.ToString(); results.AddSuccess(supplier); } else { CoreUtils.LogException("", error, $"Error while posting supplier {supplier.ID}"); results.AddFailed(supplier, error.Message); } } return results; } } public class SupplierMYOBPosterEngine : MYOBPosterEngine, IPullerEngine { public IPullResult DoPull() { LoadConnectionData(); return Poster.Pull(); } }