using Comal.Classes; using FastReport.Data; using InABox.Core; using InABox.Core.Postable; using InABox.Poster.MYOB; using MYOB.AccountRight.SDK.Services; using MYOB.AccountRight.SDK.Services.Contact; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using Customer = Comal.Classes.Customer; using MYOBCustomer = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Customer; using MYOBAddress = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Address; using MYOB.AccountRight.SDK.Contracts.Version2.Sale; using InABox.Clients; using InABox.Database; using MYOB.AccountRight.SDK; using InABox.Scripting; using Org.BouncyCastle.Bcpg.OpenPgp; namespace PRS.Shared.Posters.MYOB; public class CustomerMYOBPosterSettings : MYOBPosterSettings { public override string DefaultScript(Type TPostable) { return @"using MYOBCustomer = MYOB.AccountRight.SDK.Contracts.Version2.Contact.Customer; public class Module { public void BeforePost(IDataModel model) { // Perform pre-processing } public void ProcessCustomer(IDataModel model, Customer customer, MYOBCustomer myobCustomer) { // Do extra processing for a customer; throw an exception to fail this customer. } }"; } } public static class ContactMYOBUtils { public static void SplitName(string name, out string firstName, out string lastName) { var names = name.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); firstName = names.Length > 0 ? names[0] : ""; lastName = names.Length > 1 ? names[1] : ""; } public static Address ConvertAddress(MYOBAddress address) { var newAddress = new Address { Street = address.Street, City = address.City, State = address.State, PostCode = address.PostCode }; return newAddress; } public static MYOBAddress ConvertAddress(Address address, int location, IContact contact) { var mobile = contact.Mobile.Truncate(21); var phone = contact.Telephone.Truncate(21); var newAddress = new MYOBAddress { Location = location, Street = address.Street.Truncate(255), City = address.City.Truncate(255), State = address.State.Truncate(255), PostCode = address.PostCode.Truncate(11), Email = contact.Email.Truncate(255), ContactName = contact.Name.Truncate(25) }; if (mobile.IsNullOrWhiteSpace()) { newAddress.Phone1 = phone; } else { newAddress.Phone1 = mobile; newAddress.Phone2 = phone; } return newAddress; } } public class CustomerMYOBAutoRefresher : IAutoRefresher { public bool ShouldRepost(Customer customer) { var shouldRepost = customer.HasOriginalValue(x => x.Name) || customer.HasOriginalValue(x => x.Code) || customer.HasOriginalValue(x => x.ABN) || customer.DefaultContact.HasOriginalValue(x => x.ID) || customer.Delivery.HasOriginalValue(x => x.Street) || customer.Delivery.HasOriginalValue(x => x.City) || customer.Delivery.HasOriginalValue(x => x.State) || customer.Delivery.HasOriginalValue(x => x.PostCode) || customer.Postal.HasOriginalValue(x => x.Street) || customer.Postal.HasOriginalValue(x => x.City) || customer.Postal.HasOriginalValue(x => x.State) || customer.Postal.HasOriginalValue(x => x.PostCode); if (shouldRepost) { return true; } if(customer.CustomerStatus.HasOriginalValue(x => x.ID)) { var originalID = customer.CustomerStatus.GetOriginalValue(x => x.ID); var currentID = customer.CustomerStatus.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 CustomerMYOBPoster : IMYOBPoster, IAutoRefreshPoster { public ScriptDocument? Script { get; set; } public CustomerMYOBPosterSettings Settings { get; set; } public MYOBGlobalPosterSettings GlobalSettings { get; set; } public MYOBConnectionData ConnectionData { 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: new object[] { model }); return true; } public static Columns RequiredColumns() { return Columns.None() .Add(x => x.ID) .Add(x => x.PostedReference) .Add(x => x.PostedStatus) .Add(x => x.DefaultContact.Name) .Add(x => x.Name) .Add(x => x.Code) .Add(x => x.CustomerStatus.ID) .Add(x => x.CustomerStatus.Active) .Add(x => x.Delivery.Street) .Add(x => x.Delivery.City) .Add(x => x.Delivery.State) .Add(x => x.Delivery.PostCode) .Add(x => x.Postal.Street) .Add(x => x.Postal.City) .Add(x => x.Postal.State) .Add(x => x.Postal.PostCode) .Add(x => x.DefaultContact.Mobile) .Add(x => x.DefaultContact.Telephone) .Add(x => x.DefaultContact.Email) .Add(x => x.DefaultContact.Name) .Add(x => x.DefaultContact.Mobile) .Add(x => x.ABN); } #region Script Functions private Result ProcessCustomer(IDataModel model, Customer customer, MYOBCustomer myobCustomer) { return this.WrapScript("ProcessCustomer", model, customer, myobCustomer); } #endregion public static Result UpdateCustomer(MYOBConnectionData data, MYOBGlobalPosterSettings settings, Customer customer, MYOBCustomer myobCustomer, bool isNew) { // Documentation: https://developer.myob.com/api/myob-business-api/v2/contact/customer/ // Since this might be called from some other poster, we need to ensure we have the right columns. Client.EnsureColumns(customer, RequiredColumns()); ContactMYOBUtils.SplitName(customer.DefaultContact.Name, out var firstName, out var lastName); myobCustomer.CompanyName = customer.Name.Truncate(50); myobCustomer.FirstName = firstName.Truncate(30); myobCustomer.LastName = lastName.Truncate(20); myobCustomer.IsIndividual = false; myobCustomer.DisplayID = customer.Code.Truncate(15); // If there is not customer status, we will use default to Active = true. myobCustomer.IsActive = customer.CustomerStatus.ID == Guid.Empty || customer.CustomerStatus.Active; myobCustomer.Addresses = [ ContactMYOBUtils.ConvertAddress(customer.Postal, 1, customer.DefaultContact), ContactMYOBUtils.ConvertAddress(customer.Delivery, 2, customer.DefaultContact) ]; // Notes = // PhotoURI = // RowVersion = myobCustomer.SellingDetails ??= new(); myobCustomer.SellingDetails.SaleLayout = InvoiceLayoutType.NoDefault; // myobCustomer.SellingDetails.PrintedFOrm = myobCustomer.SellingDetails.InvoiceDelivery = DocumentAction.PrintAndEmail; // myobCustomer.SellingDetails.IncomeAccount = // myobCustomer.SellingDetails.ReceiptMemo = // myobCustomer.SellingDetails.SalesPerson = // myobCustomer.SellingDetails.SaleComment = // myobCustomer.SellingDetails.ShippingMethod = // myobCustomer.SellingDetails.HourlyBillRate = // myobCustomer.SellingDetails.ABNBranch = myobCustomer.SellingDetails.ABN = customer.ABN.Truncate(14); if (isNew) { if(!MYOBPosterUtils.GetDefaultTaxCode(data, settings).Get(out var taxID, out var error)) { return Result.Error(error); } myobCustomer.SellingDetails.TaxCode ??= new(); myobCustomer.SellingDetails.TaxCode.UID = taxID; myobCustomer.SellingDetails.FreightTaxCode ??= new(); myobCustomer.SellingDetails.FreightTaxCode.UID = taxID; } return Result.Ok(); } /// /// Try to find a customer 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 customer to map to. /// The UID of the MYOB customer. public static Result MapCustomer(MYOBConnectionData data, Customer customer, MYOBGlobalPosterSettings settings) { if(Guid.TryParse(customer.PostedReference, out var myobID)) { return new Result(myobID); } var service = new CustomerService(data.Configuration, null, data.AuthKey); var result = service.Query(data, new Filter(x => x.DisplayID).IsEqualTo(customer.Code), top: 1); return result.MapOk(customers => { if(customers.Items.Length == 0) { if(customer.Code.Length > 15) { return Result.Error(new Exception("Customer code is longer than 15 characters")); } var myobCustomer = new MYOBCustomer(); return UpdateCustomer(data, settings, customer, myobCustomer, true) .MapOk(() => service.Save(data, myobCustomer) .MapOk(x => { // Marking as repost because a script may not have run. customer.PostedStatus = PostedStatus.RequiresRepost; customer.PostedReference = x.UID.ToString(); return x.UID; })) .Flatten(); } else { customer.PostedReference = customers.Items[0].UID.ToString(); customer.PostedStatus = PostedStatus.RequiresRepost; return Result.Ok(customers.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 customerCodes = new HashSet(); var service = new CustomerService(ConnectionData.Configuration, null, ConnectionData.AuthKey); while (true) { if(!service.Query(ConnectionData, null, top: top, skip: skip).Get(out var myobCustomers, out var error)) { CoreUtils.LogException("", error); throw new PullFailedMessageException(error.Message); } if(myobCustomers.Items.Length == 0) { break; } var myobIDs = myobCustomers.Items.ToArray(x => x.UID.ToString()); var myobCodes = myobCustomers.Items.Select(x => x.DisplayID).Where(x => !IsBlankCode(x)).ToArray(); var myobNames = myobCustomers.Items.Where(x => IsBlankCode(x.DisplayID) && !x.CompanyName.IsNullOrWhiteSpace()) .Select(x => x.CompanyName).ToArray(); var customers = 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 customerDict = customers.Where(x => !x.PostedReference.IsNullOrWhiteSpace()) .ToDictionary(x => x.PostedReference); var blankCustomers = customers.Where(x => x.PostedReference.IsNullOrWhiteSpace()).ToArray(); var needCodes = new Dictionary(); foreach(var myobCustomer in myobCustomers.Items) { if (customerDict.TryGetValue(myobCustomer.UID.ToString(), out var customer)) { // Skipping existing customers at this point. continue; } customer = !IsBlankCode(myobCustomer.DisplayID) ? blankCustomers.FirstOrDefault(x => string.Equals(x.Code, myobCustomer.DisplayID)) : blankCustomers.FirstOrDefault(x => string.Equals(x.Name, myobCustomer.CompanyName)); if(customer is not null) { customer.PostedReference = myobCustomer.UID.ToString(); result.AddEntity(PullResultType.Linked, customer); continue; } customer = new Customer(); string code; if (!IsBlankCode(myobCustomer.DisplayID)) { code = myobCustomer.DisplayID.ToString(); } else if (!myobCustomer.CompanyName.IsNullOrWhiteSpace()) { code = myobCustomer.CompanyName[..Math.Min(3, myobCustomer.CompanyName.Length)].ToUpper(); } else { code = "CUS"; } int i = 1; customer.Code = code; while (customerCodes.Contains(customer.Code)) { customer.Code = $"{code}{i:d3}"; ++i; } customerCodes.Add(customer.Code); customer.Name = myobCustomer.CompanyName; customer.ABN = myobCustomer.SellingDetails.ABN; if(myobCustomer.Addresses is not null) { var delivery = myobCustomer.Addresses.FirstOrDefault(x => x.Location == 2); if(delivery is not null) { customer.Delivery.CopyFrom(ContactMYOBUtils.ConvertAddress(delivery)); } var postal = myobCustomer.Addresses.FirstOrDefault(x => x.Location == 1); if(postal is not null) { customer.Postal.CopyFrom(ContactMYOBUtils.ConvertAddress(postal)); } customer.Email = delivery?.Email ?? postal?.Email ?? ""; } customer.PostedReference = myobCustomer.UID.ToString(); result.AddEntity(PullResultType.New, customer); needCodes.Add(customer.Code, (code, i, customer)); } // Do code clash checking while(needCodes.Count > 0) { var codes = Client.Query( new Filter(x => x.Code).InList(needCodes.Values.Select(x => x.customer.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.customer.Code = $"{needed.prefix}{i:d3}"; ++i; } while (customerCodes.Contains(needed.customer.Code)); customerCodes.Add(needed.customer.Code); newNeedCodes.Add(needed.customer.Code, (needed.prefix, i, needed.customer)); } } needCodes = newNeedCodes; } skip += top; } return result; } public IPostResult Process(IDataModel model) { var results = new PostResult(); var service = new CustomerService(ConnectionData.Configuration, null, ConnectionData.AuthKey); var customers = model.GetTable().ToArray(); foreach(var customer in customers) { if(customer.Code.Length > 15) { results.AddFailed(customer, "Code is longer than 15 characters."); continue; } bool isNew; MYOBCustomer myobCustomer; Exception? error; if(Guid.TryParse(customer.PostedReference, out var myobID)) { if(!service.Get(ConnectionData, myobID).Get(out var newCustomer, out error)) { CoreUtils.LogException("", error, $"Failed to find Customer in MYOB with id {myobID}"); results.AddFailed(customer, $"Failed to find Customer in MYOB with id {myobID}: {error.Message}"); continue; } myobCustomer = newCustomer; isNew = false; } else { if(service.Query( ConnectionData, new Filter(x => x.DisplayID).IsEqualTo(customer.Code), top: 1).Get(out var myobCustomers, out error)) { if(myobCustomers.Items.Length > 0) { myobCustomer = myobCustomers.Items[0]; isNew = false; } else if(service.Query( ConnectionData, new Filter(x => x.CompanyName).IsEqualTo(customer.Name) .And(new Filter(x => x.DisplayID).IsEqualTo(null) .Or(x => x.DisplayID).IsEqualTo("") .Or(x => x.DisplayID).IsEqualTo("*None")), top: 1).Get(out myobCustomers, out error)) { if(myobCustomers.Items.Length > 0) { myobCustomer = myobCustomers.Items[0]; myobCustomer.DisplayID = customer.Code; isNew = false; } else { myobCustomer = new MYOBCustomer(); isNew = true; } } else { CoreUtils.LogException("", error); results.AddFailed(customer, error.Message); continue; } } else { CoreUtils.LogException("", error); results.AddFailed(customer, error.Message); continue; } } if(UpdateCustomer(ConnectionData, GlobalSettings, customer, myobCustomer, isNew) .MapOk(() => ProcessCustomer(model, customer, myobCustomer)).Flatten() .MapOk(() => service.Save(ConnectionData, myobCustomer)).Flatten() .Get(out var result, out error)) { customer.PostedReference = result.UID.ToString(); results.AddSuccess(customer); } else { results.AddFailed(customer, error.Message); } } return results; } } public class CustomerMYOBPosterEngine : MYOBPosterEngine, IPullerEngine { public IPullResult DoPull() { LoadConnectionData(); return Poster.Pull(); } }