Explorar el Código

Added Payment poster

Kenric Nugteren hace 2 días
padre
commit
be6bb2b9aa

+ 3 - 0
prs.classes/Entities/Bill/BillLink.cs

@@ -19,5 +19,8 @@ namespace Comal.Classes
 
         [CurrencyEditor(Editable = Editable.Hidden)]
         public decimal Balance { get; set; }
+
+        [NullEditor]
+        public string PostedReference { get; set; }
     }
 }

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

@@ -19,7 +19,7 @@ namespace Comal.Classes
     }
 
     [UserTracking(typeof(Bill))]
-    public class Payment : Entity, IPersistent, IRemotable, ILicense<AccountsPayableLicense>
+    public class Payment : Entity, IPersistent, IRemotable, ILicense<AccountsPayableLicense>, IPostable
     {
         [DateEditor]
         public DateTime Date { get; set; }
@@ -37,6 +37,20 @@ namespace Comal.Classes
         [Aggregate(typeof(PaymentTotal))]
         public decimal Total { get; set; }
 
+        [NullEditor]
+        public DateTime Posted { get; set; }
+
+        [NullEditor]
+        [RequiredColumn]
+        public PostedStatus PostedStatus { get; set; }
+
+        [NullEditor]
+        public string PostedNote { get; set; }
+
+        [NullEditor]
+        public string PostedReference { get; set; }
+
+
         public override string ToString()
         {
             return string.Format("{0:dd MMM yy}: {1} (${2:F2})", Date, PaymentType.Description, Total);

+ 246 - 0
prs.shared/Posters/Xero/PaymentXeroPoster.cs

@@ -0,0 +1,246 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Core.Postable;
+using InABox.Poster.Xero;
+using InABox.Scripting;
+using PRS.Shared.Posters.Xero.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Markup.Localizer;
+using Bill = Comal.Classes.Bill;
+using XeroPayment = Xero.NetStandard.OAuth2.Model.Accounting.BatchPayment;
+using XeroBillPayment = Xero.NetStandard.OAuth2.Model.Accounting.Payment;
+
+namespace PRS.Shared.Posters.Xero;
+
+public class PaymentXeroPosterSettings : XeroPosterSettings
+{
+    [EditorSequence(1)]
+    [Comment("The Xero account code for payments")]
+    [TextBoxEditor]
+    public string Account { get; set; }
+
+    protected override string DefaultScript()
+    {
+        return @"
+using XeroPayment = Xero.NetStandard.OAuth2.Model.Accounting.BatchPayment;
+using XeroBillPayment = Xero.NetStandard.OAuth2.Model.Accounting.Payment;
+
+// 'Module' *must* implement " + nameof(IPaymentXeroPosterScript) + @"
+public class Module : " + nameof(IPaymentXeroPosterScript) + @"
+{
+    public void " + nameof(IPaymentXeroPosterScript.BeforePost) + @"(IDataModel<Payment> model)
+    {
+        // Perform pre-processing
+    }
+
+    public void " + nameof(IPaymentXeroPosterScript.ProcessPayment) + @"(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment)
+    {
+        // Do extra processing for a payment; throw an exception to fail this payment.
+    }
+
+    public void " + nameof(IPaymentXeroPosterScript.ProcessBillPayment) + @"(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment)
+    {
+        // Do extra processing for an bill payment; throw an exception to fail this bill payment (and thus fail the entire payment)
+    }
+}";
+    }
+}
+
+public interface IPaymentXeroPosterScript
+{
+    void BeforePost(IDataModel<Payment> model);
+
+    void ProcessPayment(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment);
+
+    void ProcessBillPayment(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment);
+}
+
+public class PaymentXeroPoster : IXeroPoster<Payment, PaymentXeroPosterSettings>
+{
+    public ScriptDocument? Script { get; set; }
+    public XeroConnectionData ConnectionData { get; set; }
+    public PaymentXeroPosterSettings Settings { get; set; }
+    public IPosterDispatcher Dispatcher { get; set; }
+
+    public IPaymentXeroPosterScript? ScriptObject => Script?.GetObject<IPaymentXeroPosterScript>();
+
+    public bool BeforePost(IDataModel<Payment> model)
+    {
+        foreach(var (_, table) in model.ModelTables)
+        {
+            table.IsDefault = false;
+        }
+        model.SetIsDefault<Payment>(true);
+        model.SetColumns<Payment>(RequiredPaymentColumns());
+
+        model.SetIsDefault<BillPayment>(true, alias: "Payment_BillPayment");
+        model.SetColumns<BillPayment>(RequiredBillPaymentColumns(), alias: "Payment_BillPayment");
+
+        ScriptObject?.BeforePost(model);
+
+        return true;
+    }
+
+    #region Script Functions
+
+    private Result<Exception> ProcessPayment(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment)
+    {
+        try
+        {
+            ScriptObject?.ProcessPayment(model, payment, xeroPayment);
+            return Result.Ok();
+        }
+        catch(Exception e)
+        {
+            return Result.Error(e);
+        }
+    }
+    private Result<Exception> ProcessBillPayment(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment)
+    {
+        try
+        {
+            ScriptObject?.ProcessBillPayment(model, billPayment, xeroBillPayment);
+            return Result.Ok();
+        }
+        catch(Exception e)
+        {
+            return Result.Error(e);
+        }
+    }
+
+    #endregion
+
+    private static Columns<Payment> RequiredPaymentColumns()
+    {
+        return Columns.None<Payment>()
+            .Add(x => x.ID)
+            .Add(x => x.PostedReference)
+            .Add(x => x.PostedStatus)
+            .Add(x => x.Date)
+            .Add(x => x.Notes);
+    }
+
+    private static Columns<BillPayment> RequiredBillPaymentColumns()
+    {
+        return Columns.None<BillPayment>()
+            .Add(x => x.ID)
+            .Add(x => x.Amount)
+            .Add(x => x.Payment.ID)
+            .Add(x => x.Bill.Number)
+            .Add(x => x.Bill.PostedReference);
+    }
+
+    public IPostResult<Payment> Process(IDataModel<Payment> model)
+    {
+        // https://developer.xero.com/documentation/api/accounting/payments
+
+        var results = new PostResult<Payment>();
+
+        var service = new PaymentService(ConnectionData);
+
+        var payments = model.GetTable<Payment>().ToArray<Payment>();
+
+        var billPayments = model.GetTable<BillPayment>("Payment_BillPayment")
+            .ToObjects<BillPayment>().GroupByDictionary(x => x.Payment.ID);
+        
+        foreach(var payment in payments)
+        {
+            // For payments, always create a new one, so we cannot posted already posted stuff.
+            if(payment.PostedStatus == PostedStatus.Posted)
+            {
+                continue;
+            }
+
+            var xeroPayment = new XeroPayment();
+
+            if (Settings.Account.IsNullOrWhiteSpace())
+            {
+                throw new MissingSettingException<PaymentXeroPosterSettings>(x => x.Account);
+            }
+            else
+            {
+                xeroPayment.Account.Code = Settings.Account;
+            }
+
+            xeroPayment.Date = payment.Date;
+            xeroPayment.Details = payment.Notes.Truncate(18);
+
+            if(billPayments.TryGetValue(payment.ID, out var paymentBills))
+            {
+                xeroPayment.Payments.Clear();
+                string? failed = null;
+                for(int i = 0; i < paymentBills.Count; ++i)
+                {
+                    var bill = paymentBills[i];
+
+                    var line = new XeroBillPayment();
+                    // BankAccountNumber
+
+                    line.Amount = bill.Amount;
+                    line.Details = payment.Notes;
+
+                    if(Guid.TryParse(bill.Bill.PostedReference, out var xeroBillID))
+                    {
+                        line.Invoice.InvoiceID = xeroBillID;
+                    }
+                    else
+                    {
+                        failed = $"Bill {bill.Bill.Number} hasn't been posted yet; it must be posted before this payment can be.";
+                        break;
+                    }
+
+                    if(!ProcessBillPayment(model, bill, line).Get(out var error))
+                    {
+                        failed = error.Message;
+                        break;
+                    }
+
+                    xeroPayment.Payments.Add(line);
+                }
+
+                if(failed is not null)
+                {
+                    results.AddFailed(payment, failed);
+                    continue;
+                }
+            }
+            else
+            {
+                xeroPayment.Payments.Clear();
+            }
+
+            if(!ProcessPayment(model, payment, xeroPayment).Get(out var e))
+            {
+                results.AddFailed(payment, e.Message);
+                continue;
+            }
+
+            if(service.Save(xeroPayment).Get(out var result, out e))
+            {
+                payment.PostedReference = result.BatchPaymentID.ToString() ?? "";
+                results.AddSuccess(payment);
+            }
+            else
+            {
+                CoreUtils.LogException("", e, $"Error while posting payment {payment.ID}");
+                results.AddFailed(payment, e.Message);
+            }
+        }
+
+        return results;
+    }
+}
+
+public class PaymentXeroPosterEngine<T> : XeroPosterEngine<Payment, PaymentXeroPoster, PaymentXeroPosterSettings>
+{
+    protected override IList<string> RequiredScopes()
+    {
+        return [
+            "accounting.transactions"
+            ];
+    }
+}

+ 52 - 0
prs.shared/Posters/Xero/Services/PaymentService.cs

@@ -0,0 +1,52 @@
+using InABox.Poster.Xero;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xero.NetStandard.OAuth2.Api;
+using Xero.NetStandard.OAuth2.Model.Accounting;
+
+namespace PRS.Shared.Posters.Xero.Services;
+
+internal class PaymentService : BaseXeroService<BatchPayment, BatchPaymentWrapper>
+{
+    private AccountingApi _api = new();
+
+    public PaymentService(XeroConnectionData connection) : base(connection)
+    {
+    }
+
+    protected override void InitialiseFilterMaps()
+    {
+    }
+
+    protected override async Task<BatchPaymentWrapper> GetAsync(string accessToken, string xeroTenantID, Guid id, CancellationToken cancellationToken = default)
+    {
+        return new(await _api.GetBatchPaymentAsync(accessToken, xeroTenantID, id, cancellationToken));
+    }
+
+    protected override async Task<BatchPaymentWrapper> GetAsync(string accessToken, string xeroTenantID, string? where, Dictionary<string, List<object?>> filterMaps, int? page = null, int? pageSize = null, CancellationToken cancellationToken = default)
+    {
+        return new(await _api.GetBatchPaymentsAsync(accessToken, xeroTenantID,
+            where: where,
+            cancellationToken: cancellationToken));
+    }
+
+    protected override BatchPayment Save(BatchPayment entity)
+    {
+        var batchPayments = new BatchPayments();
+        batchPayments._BatchPayments.Add(entity);
+        return _api.CreateBatchPaymentAsync(Connection.Token.AccessToken, Connection.TenantID.ToString(), batchPayments)
+            .Result
+            ._BatchPayments[0];
+    }
+}
+
+internal class BatchPaymentWrapper(BatchPayments payments) : IXeroGetResultWrapper<BatchPayment>
+{
+    public IList<BatchPayment> GetValues()
+    {
+        return payments._BatchPayments;
+    }
+}