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; 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 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.Provider.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, 2, customer.DefaultContact), ContactMYOBUtils.ConvertAddress(customer.Delivery, 1, 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(); } 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 { myobCustomer = new MYOBCustomer(); isNew = true; } } 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 { }