Pārlūkot izejas kodu

Made GL codes and Taxcodes IPostableFragments;
added posting invoices to MYOB.
Added SellGL to InvoiceLine
Made Supplier

Kenric Nugteren 1 gadu atpakaļ
vecāks
revīzija
250219d905

+ 2 - 1
prs.classes/Entities/Customer/Customer.cs

@@ -53,7 +53,8 @@ namespace Comal.Classes
     }
 
     [UserTracking(typeof(Job))]
-    public class Customer : Entity, IPersistent, IRemotable, ISchedulable, ILicense<ProjectManagementLicense>, IExportable, IImportable, IMergeable, IPostable
+    public class Customer : Entity, IPersistent, IRemotable, ISchedulable, ILicense<ProjectManagementLicense>, IExportable, IImportable, IMergeable,
+        IPostable, IPostableFragment<Invoice>
     {
         [EditorSequence(1)]
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]

+ 4 - 1
prs.classes/Entities/GLCode/BaseGLCodeLink.cs

@@ -4,12 +4,15 @@ using InABox.Core;
 namespace Comal.Classes
 {
 
-    public abstract class BaseGLCodeLink : EntityLink<GLCode>
+    public abstract class BaseGLCodeLink : EntityLink<GLCode>, IPostableFragment
     {
         [CodeEditor(Visible = Visible.Default, Editable = Editable.Hidden)]
         public string Code { get; set; }
 
         [TextBoxEditor(Editable = Editable.Hidden)]
         public string Description { get; set; }
+
+        [NullEditor]
+        public string PostedReference { get; set; }
     }
 }

+ 4 - 1
prs.classes/Entities/GLCode/GLCode.cs

@@ -3,7 +3,7 @@
 namespace Comal.Classes
 {
     [UserTracking(typeof(Product))]
-    public class GLCode : Entity, IRemotable, IPersistent, ILicense<CoreLicense>, IExportable, IImportable, IMergeable
+    public class GLCode : Entity, IRemotable, IPersistent, ILicense<CoreLicense>, IExportable, IImportable, IMergeable, IPostableFragment<Invoice>
     {
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]
         public string Code { get; set; }
@@ -20,6 +20,9 @@ namespace Comal.Classes
         [CheckBoxEditor]
         public bool Hidden { get; set; }
 
+        [NullEditor]
+        public string PostedReference { get; set; }
+
         public override string ToString()
         {
             return string.Format("{0}: {1}", Code, Description);

+ 3 - 0
prs.classes/Entities/Invoice/Invoice.cs

@@ -131,6 +131,9 @@ namespace Comal.Classes
         [Aggregate(typeof(InvoiceAmountPaid))]
         public double AmountPaid { get; set; }
 
+        /// <summary>
+        /// Balance = <see cref="IncTax"/> - <see cref="AmountPaid"/>.
+        /// </summary>
         [EditorSequence(12)]
         [CurrencyEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Formula(typeof(InvoiceBalance))]

+ 3 - 0
prs.classes/Entities/Invoice/InvoiceLine.cs

@@ -70,6 +70,9 @@ namespace Comal.Classes
         [EditorSequence(10)]
         public CostCentreLink CostCentre { get; set; }
         
+        [EditorSequence(11)]
+        public SalesGLCodeLink SellGL { get; set; }
+        
         [NullEditor]
         public long Sequence { get; set; }
 

+ 15 - 1
prs.classes/Entities/TaxCode/TaxCode.cs

@@ -2,8 +2,19 @@
 
 namespace Comal.Classes
 {
+    public interface ITaxCode : IEntity,
+        IPostableFragment
+    {
+        string Code { get; set; }
+
+        string Description { get; set; }
+
+        double Rate { get; set; }
+    }
+
     [UserTracking(typeof(Invoice))]
-    public class TaxCode : Entity, IPersistent, IRemotable, ILicense<CoreLicense>, IExportable, IImportable, IMergeable
+    public class TaxCode : Entity, IPersistent, IRemotable, ILicense<CoreLicense>, IExportable, IImportable, IMergeable,
+        IPostableFragment<Customer>, IPostableFragment<Invoice>, ITaxCode
     {
         [EditorSequence(1)]
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]
@@ -17,6 +28,9 @@ namespace Comal.Classes
         [DoubleEditor(Visible = Visible.Default)]
         public double Rate { get; set; }
 
+        [NullEditor]
+        public string PostedReference { get; set; }
+
         public override string ToString()
         {
             return string.Format("{0}: {1} ({2:F2}%)", Code, Description, Rate);

+ 4 - 1
prs.classes/Entities/TaxCode/TaxCodeLink.cs

@@ -3,7 +3,7 @@ using InABox.Core;
 
 namespace Comal.Classes
 {
-    public class TaxCodeLink : EntityLink<TaxCode>
+    public class TaxCodeLink : EntityLink<TaxCode>, ITaxCode
     {
         [LookupEditor(typeof(TaxCode))]
         [RequiredColumn]
@@ -19,6 +19,9 @@ namespace Comal.Classes
         [RequiredColumn]
         public double Rate { get; set; }
 
+        [NullEditor]
+        public string PostedReference { get; set; }
+
         public override string ToString()
         {
             return string.Format("{0}: {1} ({2:F2}%)", Code, Description, Rate);

+ 142 - 95
prs.shared/Posters/MYOB/CustomerMYOBPoster.cs

@@ -38,7 +38,7 @@ public class CustomerMYOBPoster : IMYOBPoster<Customer, CustomerMYOBPosterSettin
         lastName = names.Length > 1 ? names[1] : "";
     }
 
-    private MYOBAddress ConvertAddress(Address address, int location, IContact contact)
+    private static MYOBAddress ConvertAddress(Address address, int location, IContact contact)
     {
         return new MYOBAddress
         {
@@ -65,10 +65,117 @@ public class CustomerMYOBPoster : IMYOBPoster<Customer, CustomerMYOBPosterSettin
         return true;
     }
 
-    public IPostResult<Customer> Process(IDataModel<Customer> model)
+    public static Result<Exception> UpdateCustomer(MYOBConnectionData data, CustomerMYOBPosterSettings settings, Customer customer, MYOBCustomer myobCustomer, bool isNew)
     {
         // Documentation: https://developer.myob.com/api/myob-business-api/v2/contact/customer/
 
+        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);
+        myobCustomer.IsActive = customer.CustomerStatus.Active;
+        myobCustomer.Addresses =
+        [
+            ConvertAddress(customer.Delivery, 1, customer.DefaultContact),
+            ConvertAddress(customer.Postal, 2, customer.DefaultContact)
+        ];
+        // Notes = 
+        // PhotoURI =
+        // RowVersion = 
+        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 (settings.DefaultTaxCode.IsNullOrWhiteSpace())
+            {
+                throw new PostFailedMessageException("Default tax code has not been set up.");
+            }
+            else if(data.GetMYOBTaxCodeUID(settings.DefaultTaxCode).Get(out var taxID, out var error))
+            {
+                if (taxID == Guid.Empty)
+                {
+                    return Result.Error(new Exception($"Failed to find TaxCode in MYOB with code {settings.DefaultTaxCode}"));
+                }
+                myobCustomer.SellingDetails.TaxCode.UID = taxID;
+                myobCustomer.SellingDetails.FreightTaxCode.UID = taxID;
+            }
+            else
+            {
+                CoreUtils.LogException("", error, $"Failed to find TaxCode in MYOB with code {settings.DefaultTaxCode}");
+                return Result.Error(new Exception($"Failed to find TaxCode in MYOB with code {settings.DefaultTaxCode}: {error.Message}", error));
+            }
+        }
+        return Result.Ok<Exception>();
+    }
+
+    /// <summary>
+    /// Try to find a customer in MYOB which matches <paramref name="customer"/>, and if this fails, create a new one.
+    /// </summary>
+    /// <remarks>
+    /// After this has finished, <paramref name="customer"/> will be updated with <see cref="Customer.PostedReference"/> set to the correct ID.
+    /// <br/>
+    /// <paramref name="customer"/> needs to have at least <see cref="Customer.Code"/> and <see cref="Customer.PostedReference"/> as loaded columns.
+    /// </remarks>
+    /// <param name="data"></param>
+    /// <param name="customer">The customer to map to.</param>
+    /// <returns>The UID of the MYOB customer.</returns>
+    public static Result<Guid, Exception> MapCustomer(MYOBConnectionData data, Customer customer)
+    {
+        if(Guid.TryParse(customer.PostedReference, out var myobID))
+        {
+            return new Result<Guid, Exception>(myobID);
+        }
+
+        var service = new CustomerService(data.Configuration, null, data.AuthKey);
+        var result = service.Query(data, new Filter<MYOBCustomer>(x => x.DisplayID).IsEqualTo(customer.Code), top: 1);
+        return result.MapOk(customers =>
+        {
+            if(customers.Count == 0)
+            {
+                if(customer.Code.Length > 15)
+                {
+                    return Result.Error<Guid, Exception>(new Exception("Customer code is longer than 15 characters"));
+                }
+                var myobCustomer = new MYOBCustomer();
+                return UpdateCustomer(data, PosterUtils.LoadPosterSettings<Customer, CustomerMYOBPosterSettings>(), customer, myobCustomer, true)
+                    .MapOk(() =>
+                    {
+                        try
+                        {
+                            var result = service.UpdateEx(data.CompanyFile, myobCustomer, data.CompanyFileCredentials);
+                            customer.PostedReference = result.UID.ToString();
+                            return Result.Ok<Guid, Exception>(result.UID);
+                        }
+                        catch (Exception e)
+                        {
+                            CoreUtils.LogException("", e, $"Error while posting customer {customer.ID}");
+                            return Result.Error<Guid, Exception>(e);
+                        }
+                    }).Flatten();
+            }
+            else
+            {
+                customer.PostedReference = customers.Items[0].UID.ToString();
+                return Result.Ok<Guid, Exception>(customers.Items[0].UID);
+            }
+        }).Flatten();
+    }
+
+    public IPostResult<Customer> Process(IDataModel<Customer> model)
+    {
         var results = new PostResult<Customer>();
 
         var service = new CustomerService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
@@ -77,108 +184,48 @@ public class CustomerMYOBPoster : IMYOBPoster<Customer, CustomerMYOBPosterSettin
         
         foreach(var customer in customers)
         {
-            try
+            if(customer.Code.Length > 15)
             {
-                bool isNew;
-                MYOBCustomer myobCustomer;
-                if(Guid.TryParse(customer.PostedReference, out var myobID))
-                {
-                    if(!service.Get(ConnectionData, myobID).Get(out var newCustomer, out var 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;
-                    }
-                    else
-                    {
-                        myobCustomer = newCustomer;
-                        isNew = false;
-                    }
-                }
-                else
+                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))
                 {
-                    myobCustomer = new MYOBCustomer();
-                    isNew = true;
+                    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
+            {
+                myobCustomer = new MYOBCustomer();
+                isNew = true;
+            }
 
-                SplitName(customer.DefaultContact.Name, out var firstName, out var lastName);
-
-                myobCustomer = new MYOBCustomer
+            if(UpdateCustomer(ConnectionData, Settings, customer, myobCustomer, isNew).Get(out error))
+            {
+                try
                 {
-                    CompanyName = customer.Name.Truncate(50),
-                    FirstName = firstName.Truncate(30),
-                    LastName = lastName.Truncate(20),
-                    IsIndividual = false,
-                    DisplayID = customer.Code.Truncate(15),
-                    IsActive = customer.CustomerStatus.Active,
-                    Addresses =
-                    [
-                        ConvertAddress(customer.Delivery, 1, customer.DefaultContact),
-                        ConvertAddress(customer.Postal, 2, customer.DefaultContact)
-                    ],
-                    // Notes = 
-                    // PhotoURI =
-                    // RowVersion = 
-                };
-                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)
+                    var result = service.Update(ConnectionData.CompanyFile, myobCustomer, ConnectionData.CompanyFileCredentials);
+                    results.AddSuccess(customer);
+                }
+                catch(Exception e)
                 {
-                    if (Settings.DefaultTaxCode.IsNullOrWhiteSpace())
-                    {
-                        throw new PostFailedMessageException("Default tax code has not been set up.");
-                    }
-                    else if(ConnectionData.GetMYOBTaxCodeUID(Settings.DefaultTaxCode).Get(out var taxID, out var error))
-                    {
-                        if (taxID.HasValue)
-                        {
-                            myobCustomer.SellingDetails.TaxCode.UID = taxID.Value;
-                            myobCustomer.SellingDetails.FreightTaxCode.UID = taxID.Value;
-                        }
-                        else
-                        {
-                            results.AddFailed(customer, $"Failed to find TaxCode in MYOB with code {Settings.DefaultTaxCode}");
-                            continue;
-                        }
-                    }
-                    else
-                    {
-                        CoreUtils.LogException("", error, $"Failed to find TaxCode in MYOB with code {Settings.DefaultTaxCode}");
-                        results.AddFailed(customer, $"Failed to find TaxCode in MYOB with code {Settings.DefaultTaxCode}: {error.Message}");
-                        continue;
-                    }
+                    CoreUtils.LogException("", e, $"Error while posting customer {customer.ID}");
+                    results.AddFailed(customer, e.Message);
                 }
-
-                // myobCustomer.SellingDetails.UseCustomerTaxCode = 
-                // myobCustomer.SellingDetails.Terms = 
-                // myobCustomer.SellingDetails.Credit = 
-                // myobCustomer.SellingDetails.TaxIdNumber = 
-                // myobCustomer.SellingDetails.Memo = 
-                // myboCustomer.PaymentDetails.Method = 
-                // myboCustomer.PaymentDetails.CardNumber = 
-                // myboCustomer.PaymentDetails.NameOnCard = 
-                // myboCustomer.PaymentDetails.BSBNumber = 
-                // myboCustomer.PaymentDetails.BankAccountNumber = 
-                // myboCustomer.PaymentDetails.BankAccountName = 
-                // myboCustomer.PaymentDetails.Notes = 
-
-                var result = service.Update(ConnectionData.CompanyFile, myobCustomer, ConnectionData.CompanyFileCredentials);
-                results.AddSuccess(customer);
             }
-            catch (Exception e)
+            else
             {
-                CoreUtils.LogException("", e, $"Error while posting customer {customer.ID}");
-                results.AddFailed(customer, e.Message);
+                results.AddFailed(customer, error.Message);
             }
         }
 

+ 214 - 0
prs.shared/Posters/MYOB/InvoiceMYOBPoster.cs

@@ -0,0 +1,214 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Poster.MYOB;
+using MYOB.AccountRight.SDK.Contracts.Version2.Sale;
+using MYOB.AccountRight.SDK.Services.Contact;
+using MYOB.AccountRight.SDK.Services.GeneralLedger;
+using MYOB.AccountRight.SDK.Services.Sale;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Invoice = Comal.Classes.Invoice;
+using InvoiceLine = Comal.Classes.InvoiceLine;
+using MYOBAccount = MYOB.AccountRight.SDK.Contracts.Version2.GeneralLedger.Account;
+using MYOBInvoice = MYOB.AccountRight.SDK.Contracts.Version2.Sale.ServiceInvoice;
+using MYOBInvoiceLine = MYOB.AccountRight.SDK.Contracts.Version2.Sale.ServiceInvoiceLine;
+using MYOBTaxCode = MYOB.AccountRight.SDK.Contracts.Version2.GeneralLedger.TaxCode;
+
+namespace PRS.Shared.Posters.MYOB;
+
+public class InvoiceMYOBPosterSettings : MYOBPosterSettings
+{
+}
+
+public class InvoiceMYOBPoster : IMYOBPoster<Invoice, InvoiceMYOBPosterSettings>
+{
+    public MYOBConnectionData ConnectionData { get; set; }
+    public InvoiceMYOBPosterSettings Settings { get; set; }
+    public MYOBGlobalPosterSettings GlobalSettings { get; set; }
+
+    public bool BeforePost(IDataModel<Invoice> model)
+    {
+        foreach (var (_, table) in model.ModelTables)
+        {
+            table.IsDefault = false;
+        }
+        model.SetIsDefault<Invoice>(true);
+        model.SetIsDefault<InvoiceLine>(true, alias: "Invoice_InvoiceLine");
+        model.SetIsDefault<Customer>(true, alias: "Invoice_Customer");
+
+        return true;
+    }
+
+    public IPostResult<Invoice> Process(IDataModel<Invoice> model)
+    {
+        // Documentation: https://developer.myob.com/api/myob-business-api/v2/sale/invoice/invoice_service/
+        var results = new PostResult<Invoice>();
+
+        var service = new ServiceInvoiceService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
+
+        var invoices = model.GetTable<Invoice>().ToArray<Invoice>();
+        var customers = model.GetTable<Customer>("Invoice_Customer")
+            .ToObjects<Customer>().ToDictionary(x => x.ID);
+        var invoiceLines = model.GetTable<InvoiceLine>("Invoice_InvoiceLine")
+            .ToObjects<InvoiceLine>()
+            .GroupBy(x => x.InvoiceLink.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+
+        foreach(var invoice in invoices)
+        {
+            MYOBInvoice myobInvoice;
+            if(Guid.TryParse(invoice.PostedReference, out var myobID))
+            {
+                if(!service.Get(ConnectionData, myobID).Get(out var newInvoice, out var error))
+                {
+                    CoreUtils.LogException("", error, $"Failed to find Invoice in MYOB with id {myobID}");
+                    results.AddFailed(invoice, $"Failed to find Invoice in MYOB with id {myobID}: {error.Message}");
+                    continue;
+                }
+                myobInvoice = newInvoice;
+            }
+            else
+            {
+                myobInvoice = new MYOBInvoice();
+            }
+
+            myobInvoice.Number = invoice.Number.ToString().Truncate(13);
+            myobInvoice.Date = invoice.Date;
+            // myobInvoice.CustomerPurchaseOrderNumber = 
+
+            if(customers.TryGetValue(invoice.CustomerLink.ID, out var customer))
+            {
+                if(!CustomerMYOBPoster.MapCustomer(ConnectionData, customer).Get(out var customerID, out var error))
+                {
+                    CoreUtils.LogException("", error, $"Error while posting invoice {invoice.ID}");
+                    results.AddFailed(invoice, error.Message);
+                    continue;
+                }
+                myobInvoice.Customer.UID = customerID;
+            }
+            // myobInvoice.PromisedDate = 
+            myobInvoice.BalanceDueAmount = (decimal)invoice.Balance;
+            // BalanceDueAmountForeign
+            // Status
+            if(invoiceLines.TryGetValue(invoice.ID, out var lines))
+            {
+                var newLines = new MYOBInvoiceLine[lines.Length];
+
+                string? failed = null;
+                for(int i = 0; i < lines.Length; ++i)
+                {
+                    var item = lines[i];
+
+                    var line = new MYOBInvoiceLine();
+                    line.Type = InvoiceLineType.Transaction;
+                    line.Description = item.Description;
+                    // line.UnitOfMeasure = 
+                    // line.UnitCount = 
+                    // line.UnitPrice = 
+                    // line.UnitPriceForeign = 
+                    // line.DiscountPercent = 
+                    line.Total = (decimal)item.IncTax;
+                    line.TotalForeign = 0;
+
+                    if(item.SellGL.ID == Guid.Empty)
+                    {
+                        failed = "Not all lines have a SellGL code set.";
+                        break;
+                    }
+                    if(!Guid.TryParse(item.SellGL.PostedReference, out var accountID))
+                    {
+                        if (!ConnectionData.GetUID<AccountService, MYOBAccount>(
+                                new Filter<MYOBAccount>(x => x.DisplayID).IsEqualTo(item.SellGL.Code))
+                            .Get(out accountID, out var error))
+                        {
+                            CoreUtils.LogException("", error, $"Failed to find Account in MYOB with code {item.SellGL.Code}");
+                            failed = $"Failed to find Account in MYOB with code {item.SellGL.Code}: {error.Message}";
+                            break;
+                        }
+                        else if (accountID == Guid.Empty)
+                        {
+                            failed = $"Failed to find Account in MYOB with code {item.SellGL.Code}";
+                            break;
+                        }
+                        else
+                        {
+                            results.AddFragment(new GLCode { ID = item.SellGL.ID, PostedReference = accountID.ToString() });
+                        }
+                    }
+                    line.Account.UID = accountID;
+
+                    if(item.TaxCode.ID == Guid.Empty)
+                    {
+                        failed = "Not all lines have a TaxCode set.";
+                        break;
+                    }
+                    if(!Guid.TryParse(item.TaxCode.PostedReference, out var taxCodeID))
+                    {
+                        if (!ConnectionData.GetUID<TaxCodeService, MYOBTaxCode>(
+                                new Filter<MYOBTaxCode>(x => x.Code).IsEqualTo(item.TaxCode.Code))
+                            .Get(out taxCodeID, out var error))
+                        {
+                            CoreUtils.LogException("", error, $"Failed to find TaxCode in MYOB with code {item.TaxCode.Code}");
+                            failed = $"Failed to find TaxCode in MYOB with code {item.TaxCode.Code}: {error.Message}";
+                            break;
+                        }
+                        else if (taxCodeID == Guid.Empty)
+                        {
+                            failed = $"Failed to find TaxCode in MYOB with code {item.TaxCode.Code}";
+                            break;
+                        }
+                        results.AddFragment(new TaxCode { ID = taxCodeID, PostedReference = taxCodeID.ToString() });
+                    }
+                    line.TaxCode.UID = taxCodeID;
+
+                    newLines[i] = line;
+                }
+                if(failed is not null)
+                {
+                    results.AddFailed(invoice, failed);
+                    continue;
+                }
+
+                myobInvoice.Lines = newLines;
+            }
+            else
+            {
+                myobInvoice.Lines = [];
+            }
+            // ShipToAddress
+            // Terms
+            myobInvoice.IsTaxInclusive = true;
+            // Freight
+            // FreightTaxCode
+            // Category
+            // Salesperson
+            myobInvoice.Comment = invoice.Description;
+            // ShippingMethod
+            // JournalMemo
+            // ReferralSource
+            // InvoiceDeliveryStatus
+            // CanApplySurcharge
+            myobInvoice.InvoiceType = InvoiceLayoutType.Service;
+            // Order
+            // OnlinePaymentMethod
+
+            try
+            {
+                var result = service.Update(ConnectionData.CompanyFile, myobInvoice, ConnectionData.CompanyFileCredentials);
+                results.AddSuccess(invoice);
+            }
+            catch (Exception e)
+            {
+                CoreUtils.LogException("", e, $"Error while posting invoice {invoice.ID}");
+                results.AddFailed(invoice, e.Message);
+            }
+        }
+
+        return results;
+    }
+}
+
+public class InvoiceMYOBPosterEngine : MYOBPosterEngine<Invoice, InvoiceMYOBPosterSettings> { }

+ 22 - 10
prs.shared/Posters/MYOB/PRSMYOBPosterUtils.cs

@@ -92,26 +92,38 @@ public static class PRSMYOBPosterUtils
         }
     }
 
-    public static Result<Guid?, Exception> GetMYOBTaxCodeUID(this MYOBConnectionData data, string code)
+    public static TService CreateService<TService, TEntity>(this MYOBConnectionData data)
+        where TService : MutableService<TEntity>
+        where TEntity : MYOBBaseEntity
     {
-        var service = new TaxCodeService(data.Configuration, null, data.AuthKey);
-        var result = service.Query(data, new Filter<MYOBTaxCode>(x => x.Code).IsEqualTo(code), top: 1);
-        if(!result.Get(out var myobCodes, out var error))
+        return (Activator.CreateInstance(typeof(TService), data.Configuration, null, data.AuthKey) as TService)!;
+    }
+
+    public static Result<Guid, Exception> GetUID<TService, TEntity>(this MYOBConnectionData data, Filter<TEntity> filter)
+        where TService : MutableService<TEntity>
+        where TEntity : MYOBBaseEntity
+    {
+        var service = data.CreateService<TService, TEntity>();
+        var result = service.Query(data, filter, top: 1);
+        if(!result.Get(out var items, out var error))
         {
-            return Result.Error<Guid?, Exception>(error);
+            return Result.Error<Guid, Exception>(error);
         }
-        if(myobCodes.Count == 0)
+        if(items.Count == 0)
         {
-            return Result.Ok<Guid?, Exception>(Guid.Empty);
+            return Result.Ok<Guid, Exception>(Guid.Empty);
         }
-        return Result.Ok<Guid?, Exception>(myobCodes.Items[0].UID);
+        return Result.Ok<Guid, Exception>(items.Items[0].UID);
     }
 
-    public static Result<Guid?, Exception> GetMYOBTaxCodeUID(this MYOBConnectionData data, ITaxCode code)
+    public static Result<Guid, Exception> GetMYOBTaxCodeUID(this MYOBConnectionData data, string code)
+        => data.GetUID<TaxCodeService, MYOBTaxCode>(new Filter<MYOBTaxCode>(x => x.Code).IsEqualTo(code));
+
+    public static Result<Guid, Exception> GetMYOBTaxCodeUID(this MYOBConnectionData data, ITaxCode code)
     {
         if(Guid.TryParse(code.PostedReference, out var id))
         {
-            return Result.Ok<Guid?, Exception>(id);
+            return Result.Ok<Guid, Exception>(id);
         }
         return GetMYOBTaxCodeUID(data, code.Code);
     }