瀏覽代碼

- Added requirement for bills to be approved when posting to MYOB.
- Added MYOB poster for purchase orders

Kenric Nugteren 11 月之前
父節點
當前提交
4eb5e3a94e

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

@@ -4,7 +4,7 @@ namespace Comal.Classes
 {
     [UserTracking(typeof(Product))]
     public class GLCode : Entity, IRemotable, IPersistent, ILicense<CoreLicense>, IExportable, IImportable, IMergeable,
-        IPostableFragment<Invoice>, IPostableFragment<Bill>
+        IPostableFragment<Invoice>, IPostableFragment<Bill>, IPostableFragment<PurchaseOrder>
     {
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]
         public string Code { get; set; }

+ 1 - 1
prs.classes/Entities/Supplier/Supplier.cs

@@ -55,7 +55,7 @@ namespace Comal.Classes
 
     [UserTracking(typeof(Bill))]
     public class Supplier : Entity, IPersistent, IRemotable, ILicense<CoreLicense>, IExportable, IImportable, IMergeable, IPostable,
-        IPostableFragment<Bill>
+        IPostableFragment<Bill>, IPostableFragment<PurchaseOrder>
     {
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]
         [EditorSequence(1)]

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

@@ -14,7 +14,7 @@ namespace Comal.Classes
 
     [UserTracking(typeof(Invoice))]
     public class TaxCode : Entity, IPersistent, IRemotable, ILicense<CoreLicense>, IExportable, IImportable, IMergeable,
-        IPostableFragment<Customer>, IPostableFragment<Invoice>, IPostableFragment<Bill>, ITaxCode
+        IPostableFragment<Customer>, IPostableFragment<Invoice>, IPostableFragment<Bill>, IPostableFragment<PurchaseOrder>, ITaxCode
     {
         [EditorSequence(1)]
         [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]

+ 7 - 2
prs.shared/Posters/MYOB/BillMYOBPoster.cs

@@ -91,6 +91,7 @@ public class BillMYOBPoster : IMYOBPoster<Bill, BillMYOBPosterSettings>
     {
         return Columns.None<Bill>()
             .Add(x => x.ID)
+            .Add(x => x.Approved)
             .Add(x => x.PostedReference)
             .Add(x => x.Number)
             .Add(x => x.AccountingDate)
@@ -125,6 +126,10 @@ public class BillMYOBPoster : IMYOBPoster<Bill, BillMYOBPosterSettings>
         var service = new ServiceBillService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
 
         var bills = model.GetTable<Bill>().ToArray<Bill>();
+        if(bills.Any(x => x.Approved.IsEmpty()))
+        {
+            throw new PostFailedMessageException("We can't process unapproved bills; please approve all bills before processing.");
+        }
 
         var suppliers = model.GetTable<Supplier>("Bill_Supplier")
             .ToObjects<Supplier>().ToDictionary(x => x.ID);
@@ -209,7 +214,7 @@ public class BillMYOBPoster : IMYOBPoster<Bill, BillMYOBPosterSettings>
 
                     var line = new MYOBBillLine();
                     line.Type = InvoiceLineType.Transaction;
-                    line.Description = billLine.Description;
+                    line.Description = billLine.Description.Truncate(1000);
 
                     if(billLine.PurchaseGL.ID == Guid.Empty)
                     {
@@ -299,7 +304,7 @@ public class BillMYOBPoster : IMYOBPoster<Bill, BillMYOBPosterSettings>
             }
             else
             {
-                CoreUtils.LogException("", error, $"Error while posting receipt {bill.ID}");
+                CoreUtils.LogException("", error, $"Error while posting purchase order {bill.ID}");
                 results.AddFailed(bill, error.Message);
             }
         }

+ 310 - 0
prs.shared/Posters/MYOB/PurchaseOrderMYOBPoster.cs

@@ -0,0 +1,310 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Core.Postable;
+using InABox.Poster.MYOB;
+using InABox.Scripting;
+using MYOB.AccountRight.SDK.Contracts.Version2.Sale;
+using MYOB.AccountRight.SDK.Services.GeneralLedger;
+using MYOB.AccountRight.SDK.Services.Purchase;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MYOBPurchaseOrder = MYOB.AccountRight.SDK.Contracts.Version2.Purchase.ServicePurchaseOrder;
+using MYOBPurchaseOrderItem = MYOB.AccountRight.SDK.Contracts.Version2.Purchase.ServicePurchaseOrderLine;
+using MYOBTaxCode = MYOB.AccountRight.SDK.Contracts.Version2.GeneralLedger.TaxCode;
+
+namespace PRS.Shared.Posters.MYOB;
+
+public class PurchaseOrderMYOBPosterSettings : MYOBPosterSettings
+{
+    public override string DefaultScript(Type TPostable)
+    {
+        return @"using MYOBPurchaseOrder = MYOB.AccountRight.SDK.Contracts.Version2.Purchase.ServicePurchaseOrder;
+using MYOBPurchaseOrderItem = MYOB.AccountRight.SDK.Contracts.Version2.Purchase.ServicePurchaseOrderLine;
+
+public class Module
+{
+    public void BeforePost(IDataModel<PurchaseOrder> model)
+    {
+        // Perform pre-processing
+    }
+
+    public void ProcessPurchaseOrder(IDataModel<PurchaseOrder> model, PurchaseOrder purchaseOrder, MYOBPurchaseOrder myobPurchaseOrder)
+    {
+        // Do extra processing for a purchase order; throw an exception to fail this purchase order.
+    }
+
+    public void ProcessPurchaseOrderItem(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, MYOBPurchaseOrderItem myobPurchaseOrderItem)
+    {
+        // Do extra processing for a purchase order item; throw an exception to fail this purchase order item (and thus fail the entire purchase order)
+    }
+}";
+    }
+}
+
+public class PurchaseOrderMYOBPoster : IMYOBPoster<PurchaseOrder, PurchaseOrderMYOBPosterSettings>
+{
+    public ScriptDocument? Script { get; set; }
+    public MYOBConnectionData ConnectionData { get; set; }
+    public PurchaseOrderMYOBPosterSettings Settings { get; set; }
+    public MYOBGlobalPosterSettings GlobalSettings { get; set; }
+
+    public bool BeforePost(IDataModel<PurchaseOrder> model)
+    {
+        foreach (var (_, table) in model.ModelTables)
+        {
+            table.IsDefault = false;
+        }
+        model.SetIsDefault<PurchaseOrder>(true);
+        model.SetColumns<PurchaseOrder>(RequiredPurchaseOrderColumns());
+
+        model.SetIsDefault<PurchaseOrderItem>(true, alias: "PurchaseOrder_PurchaseOrderItem");
+        model.SetColumns<PurchaseOrderItem>(RequiredPurchaseOrderItemColumns(), alias: "PurchaseOrder_PurchaseOrderItem");
+
+        model.SetIsDefault<Supplier>(true, alias: "PurchaseOrder_Supplier");
+        model.SetColumns<Supplier>(RequiredSupplierColumns(), alias: "PurchaseOrder_Supplier");
+
+        Script?.Execute(methodname: "BeforePost", parameters: new object[] { model });
+
+        return true;
+    }
+
+    #region Script Functions
+
+    private Result<Exception> ProcessPurchaseOrder(IDataModel<PurchaseOrder> model, PurchaseOrder po, MYOBPurchaseOrder myobPO)
+    {
+        return this.WrapScript("ProcessPurchaseOrder", model, po, myobPO);
+    }
+    private Result<Exception> ProcessPurchaseOrderItem(IDataModel<PurchaseOrder> model, PurchaseOrderItem poItem, MYOBPurchaseOrderItem myobPOItem)
+    {
+        return this.WrapScript("ProcessPurchaseOrderItem", model, poItem, myobPOItem);
+    }
+
+    #endregion
+
+    private static Columns<PurchaseOrder> RequiredPurchaseOrderColumns()
+    {
+        return Columns.None<PurchaseOrder>()
+            .Add(x => x.ID)
+            .Add(x => x.PostedReference)
+            .Add(x => x.IssuedDate)
+            .Add(x => x.PONumber)
+            .Add(x => x.SupplierLink.ID)
+            .Add(x => x.Description);
+    }
+    private static Columns<PurchaseOrderItem> RequiredPurchaseOrderItemColumns()
+    {
+        return Columns.None<PurchaseOrderItem>()
+            .Add(x => x.ID)
+            .Add(x => x.PurchaseOrderLink.ID)
+            .Add(x => x.Description)
+            .Add(x => x.IncTax)
+            .Add(x => x.PurchaseGL.ID)
+            .Add(x => x.PurchaseGL.Code)
+            .Add(x => x.PurchaseGL.PostedReference)
+            .Add(x => x.TaxCode.ID)
+            .Add(x => x.TaxCode.Code)
+            .Add(x => x.TaxCode.PostedReference);
+    }
+    private static Columns<Supplier> RequiredSupplierColumns()
+    {
+        return SupplierMYOBPoster.RequiredColumns();
+    }
+
+    public IPostResult<PurchaseOrder> Process(IDataModel<PurchaseOrder> model)
+    {
+        var results = new PostResult<PurchaseOrder>();
+        var service = new ServicePurchaseOrderService(ConnectionData.Configuration, null, ConnectionData.AuthKey);
+
+        var pos = model.GetTable<PurchaseOrder>().ToArray<PurchaseOrder>();
+        if(pos.Any(x => x.IssuedDate.IsEmpty()))
+        {
+            throw new PostFailedMessageException($"We can't process unissued purchase orders; please set [{nameof(PurchaseOrder.IssuedDate)}] for" +
+                $" all purchase orders before processing.");
+        }
+
+        var poItems = model.GetTable<PurchaseOrderItem>("PurchaseOrder_PurchaseOrderItem")
+            .ToObjects<PurchaseOrderItem>().GroupBy(x => x.PurchaseOrderLink.ID).ToDictionary(x => x.Key, x => x.ToArray());
+
+        var suppliers = model.GetTable<Supplier>("PurchaseOrder_Supplier")
+            .ToObjects<Supplier>().ToDictionary(x => x.ID);
+
+        foreach(var po in pos)
+        {
+            MYOBPurchaseOrder myobPO;
+            Exception? error;
+            bool isNew;
+            if(Guid.TryParse(po.PostedReference, out var myobID))
+            {
+                if(!service.Get(ConnectionData, myobID).Get(out var newPO, out error))
+                {
+                    CoreUtils.LogException("", error, $"Failed to find Purchase Order in MYOB with id {myobID}");
+                    results.AddFailed(po, $"Failed to find Purchase Order in MYOB with id {myobID}: {error.Message}");
+                    continue;
+                }
+                myobPO = newPO;
+                isNew = false;
+            }
+            else
+            {
+                myobPO = new MYOBPurchaseOrder();
+                isNew = true;
+            }
+
+            myobPO.Number = po.PONumber.Truncate(13);
+            myobPO.Date = po.IssuedDate;
+            // myobPO.SupplierInvoiceNumber
+
+            if(suppliers.TryGetValue(po.SupplierLink.ID, out var supplier))
+            {
+                if(!SupplierMYOBPoster.MapSupplier(ConnectionData, supplier, GlobalSettings).Get(out var supplierID, out error))
+                {
+                    CoreUtils.LogException("", error, $"Error while posting purchase order {po.ID}");
+                    results.AddFailed(po, error.Message);
+                    continue;
+                }
+                myobPO.Supplier ??= new();
+                myobPO.Supplier.UID = supplierID;
+                results.AddFragment(supplier, supplier.PostedStatus);
+            }
+
+            // myobPO.ShipToAddress = 
+            // myobPO.Terms = 
+            myobPO.IsTaxInclusive = true;
+            myobPO.IsReportable = true;
+
+            // myobPO.Freight = 
+            // myobPO.FreightForeign = 
+
+            if (isNew)
+            {
+                if(!this.GetDefaultTaxCode().Get(out var taxCodeID, out error))
+                {
+                    results.AddFailed(po, error.Message);
+                    continue;
+                }
+                myobPO.FreightTaxCode ??= new();
+                myobPO.FreightTaxCode.UID = taxCodeID;
+            }
+
+            // myobPO.Category = 
+            myobPO.Comment = po.Description.Truncate(2000);
+            // myobPO.ShippingMethod = 
+            // myobPO.PromisedDate = 
+            // myobPO.JournalMemo = 
+            // myobPO.OrderDeliveryStatus = 
+
+            if(poItems.TryGetValue(po.ID, out var lines))
+            {
+                var newLines = new MYOBPurchaseOrderItem[lines.Length];
+                string? failed = null;
+                for(int i = 0; i < lines.Length; ++i)
+                {
+                    var poItem = lines[i];
+
+                    var line = new MYOBPurchaseOrderItem();
+                    line.Type = OrderLineType.Transaction;
+                    line.Description = poItem.Description.Truncate(1000);
+
+                    if(poItem.PurchaseGL.ID == Guid.Empty)
+                    {
+                        failed = "Not all lines have a PurchaseGL code set.";
+                        break;
+                    }
+                    if(!Guid.TryParse(poItem.PurchaseGL.PostedReference, out var accountID))
+                    {
+                        if(AccountMYOBUtils.GetAccount(ConnectionData, poItem.PurchaseGL.Code).Get(out accountID, out error))
+                        {
+                            results.AddFragment(new GLCode { ID = poItem.PurchaseGL.ID, PostedReference = accountID.ToString() });
+                        }
+                        else
+                        {
+                            CoreUtils.LogException("", error);
+                            failed = error.Message;
+                            break;
+                        }
+                    }
+                    line.Account ??= new();
+                    line.Account.UID = accountID;
+                    line.Total = (decimal)poItem.IncTax;
+                    line.TotalForeign = 0;
+                    // line.UnitsOfMeasure =
+                    // line.UnitCount =
+                    // line.UnitPrice =
+                    // line.UnitPriceForeign =
+                    // line.DiscountPercent =
+                    // line.Job =
+
+                    if(poItem.TaxCode.ID == Guid.Empty)
+                    {
+                        failed = "Not all lines have a TaxCode set.";
+                        break;
+                    }
+                    if(!Guid.TryParse(poItem.TaxCode.PostedReference, out var taxCodeID))
+                    {
+                        if (!ConnectionData.GetUID<TaxCodeService, MYOBTaxCode>(
+                                new Filter<MYOBTaxCode>(x => x.Code).IsEqualTo(poItem.TaxCode.Code))
+                            .Get(out taxCodeID, out error))
+                        {
+                            CoreUtils.LogException("", error, $"Failed to find TaxCode in MYOB with code {poItem.TaxCode.Code}");
+                            failed = $"Failed to find TaxCode in MYOB with code {poItem.TaxCode.Code}: {error.Message}";
+                            break;
+                        }
+                        else if (taxCodeID == Guid.Empty)
+                        {
+                            failed = $"Failed to find TaxCode in MYOB with code {poItem.TaxCode.Code}";
+                            break;
+                        }
+                        results.AddFragment(new TaxCode { ID = taxCodeID, PostedReference = taxCodeID.ToString() });
+                    }
+                    line.TaxCode ??= new();
+                    line.TaxCode.UID = taxCodeID;
+
+                    if(!ProcessPurchaseOrderItem(model, poItem, line).Get(out error))
+                    {
+                        failed = error.Message;
+                        break;
+                    }
+
+                    newLines[i] = line;
+                }
+
+                if(failed is not null)
+                {
+                    results.AddFailed(po, failed);
+                    continue;
+                }
+                myobPO.Lines = newLines;
+            }
+            else
+            {
+                myobPO.Lines = [];
+            }
+
+            if(!ProcessPurchaseOrder(model, po, myobPO).Get(out error))
+            {
+                results.AddFailed(po, error.Message);
+                continue;
+            }
+
+            if(service.Save(ConnectionData, myobPO).Get(out var result, out error))
+            {
+                po.PostedReference = result.UID.ToString();
+                results.AddSuccess(po);
+            }
+            else
+            {
+                CoreUtils.LogException("", error, $"Error while posting purchase order {po.ID}");
+                results.AddFailed(po, error.Message);
+            }
+        }
+
+        return results;
+    }
+}
+
+public class PurchaseOrderMYOBPosterEngine : MYOBPosterEngine<PurchaseOrder, PurchaseOrderMYOBPoster, PurchaseOrderMYOBPosterSettings>
+{
+}