ソースを参照

Poster Updates to return lists of entities, and now the Purchase Order export can read the JCCREJECT.JCC File

Kenric Nugteren 2 年 前
コミット
ff4ee82562

+ 1 - 1
prs.classes/Entities/Bill/BillLine.cs

@@ -10,7 +10,7 @@ namespace Comal.Classes
     }
 
     [UserTracking(typeof(Bill))]
-    public class BillLine : Entity, IPersistent, IRemotable, IOneToMany<Bill>, ITaxable, ILicense<AccountsPayableLicense>, IPostableFragment
+    public class BillLine : Entity, IPersistent, IRemotable, IOneToMany<Bill>, ITaxable, ILicense<AccountsPayableLicense>, IPostableFragment<Bill>
     {
         [EntityRelationship(DeleteAction.Cascade)]
         [NullEditor]

+ 1 - 1
prs.classes/Entities/PurchaseOrder/PurchaseOrderItem.cs

@@ -39,7 +39,7 @@ namespace Comal.Classes
     [UserTracking(typeof(Bill))]
     [Caption("Purchase Order Items")]
     public class PurchaseOrderItem : StockEntity, IRemotable, IPersistent, IOneToMany<PurchaseOrder>, ITaxable, IOneToMany<Consignment>, IOneToMany<Job>,
-        ILicense<AccountsPayableLicense>, IJobMaterial, IPostableFragment
+        ILicense<AccountsPayableLicense>, IJobMaterial, IPostableFragment<PurchaseOrder>
     {
         [EntityRelationship(DeleteAction.SetNull)]
         [NullEditor]

+ 28 - 3
prs.desktop/Utils/PostUtils.cs

@@ -23,14 +23,39 @@ namespace PRSDesktop
         {
             try
             {
-                if (PosterUtils.Process(model))
+                var result = PosterUtils.Process(model);
+                if(result is null)
                 {
-                    MessageBox.Show("Processing successful!");
+                    MessageBox.Show("Processing failed.");
                     refresh();
                 }
                 else
                 {
-                    MessageBox.Show("Processing failed.");
+                    var failedMessages = new List<string>();
+                    var successCount = 0;
+                    foreach(var entity in result.PostedEntities)
+                    {
+                        if(entity.PostedStatus == PostedStatus.PostFailed)
+                        {
+                            failedMessages.Add(entity.PostedNote);
+                        }
+                        else
+                        {
+                            successCount++;
+                        }
+                    }
+                    if(successCount == 0)
+                    {
+                        MessageBox.Show($"Processing failed: {string.Join('\n', failedMessages)}");
+                    }
+                    else if(failedMessages.Count == 0)
+                    {
+                        MessageBox.Show($"Processing successful; {successCount} items processed");
+                    }
+                    else
+                    {
+                        MessageBox.Show($"{successCount} items succeeded, but {failedMessages.Count} failed: {string.Join('\n', failedMessages)}");
+                    }
                     refresh();
                 }
             }

+ 4 - 4
prs.shared/Posters/CSV/BillCSVPoster.cs

@@ -16,9 +16,9 @@ namespace PRS.Shared
             return true;
         }
 
-        public ICSVExport Process(IDataModel<Bill> model)
+        public ICSVExport<Bill> Process(IDataModel<Bill> model)
         {
-            var export = new CSVExport<Bill>();
+            var export = new CSVExport<Bill, Bill>();
             export.DefineMapping(new()
             {
                 new("Number", x => x.Number),
@@ -30,11 +30,11 @@ namespace PRS.Shared
             });
             foreach (var bill in model.GetTable<Bill>().ToObjects<Bill>())
             {
-                export.Add(bill);
+                export.AddSuccess(bill, bill);
             }
             return export;
         }
-        public void AfterPost(IDataModel<Bill> model)
+        public void AfterPost(IDataModel<Bill> model, IPostResult<Bill> result)
         {
         }
 

+ 4 - 4
prs.shared/Posters/CSV/InvoiceCSVPoster.cs

@@ -16,9 +16,9 @@ namespace PRS.Shared
             return true;
         }
 
-        public ICSVExport Process(IDataModel<Invoice> model)
+        public ICSVExport<Invoice> Process(IDataModel<Invoice> model)
         {
-            var export = new CSVExport<Invoice>();
+            var export = new CSVExport<Invoice, Invoice>();
             export.DefineMapping(new()
             {
                 new("Number", x => x.Number),
@@ -30,11 +30,11 @@ namespace PRS.Shared
             });
             foreach (var invoice in model.GetTable<Invoice>().ToObjects<Invoice>())
             {
-                export.Add(invoice);
+                export.AddSuccess(invoice, invoice);
             }
             return export;
         }
-        public void AfterPost(IDataModel<Invoice> model)
+        public void AfterPost(IDataModel<Invoice> model, IPostResult<Invoice> result)
         {
         }
 

+ 90 - 66
prs.shared/Posters/Timberline/BillTimberlinePoster.cs

@@ -2,7 +2,7 @@
 using ControlzEx.Standard;
 using CsvHelper;
 using CsvHelper.Configuration.Attributes;
-using FastReport.DevComponents.WinForms.Drawing;
+using FastReport.Utils;
 using InABox.Core;
 using InABox.Core.Postable;
 using InABox.Poster.Timberline;
@@ -242,31 +242,35 @@ public class Module
         // Perform pre-processing
     }
 
-    public void ProcessHeader(IDataModel<Bill> model, Bill bill, BillTimberlineHeader header)
+    public bool ProcessHeader(IDataModel<Bill> model, Bill bill, BillTimberlineHeader header)
     {
-        // Do extra processing for a header line
+        // Do extra processing for a header line; return false to fail this header
+        return true;
     }
 
-    public void ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
+    public bool ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
     {
-        // Do extra processing for a distribution line
+        // Do extra processing for a distribution line; return false to fail this header
+        return true;
     }
 
     public void AfterPost(IDataModel<Bill> model)
     {
-        // Perform post-processing
+        // Perform post-processing;
     }
 }";
         }
     }
 
+    public class BillTimberlineResult : TimberlinePostResult<BillTimberlineHeader, Bill>
+    {
+    }
+
     public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettings>
     {
         public ScriptDocument? Script { get; set; }
         public BillTimberlineSettings Settings { get; set; }
 
-        public event ITimberlinePoster<Bill, BillTimberlineSettings>.AddFragmentCallback? AddFragment;
-
         public bool BeforePost(IDataModel<Bill> model)
         {
             model.SetIsDefault<Document>(false, alias: "CompanyLogo");
@@ -304,18 +308,18 @@ public class Module
             return true;
         }
 
-        private void ProcessHeader(IDataModel<Bill> model, Bill bill, BillTimberlineHeader header)
+        private bool ProcessHeader(IDataModel<Bill> model, Bill bill, BillTimberlineHeader header)
         {
-            Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, bill, header });
+            return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, bill, header }) != false;
         }
-        private void ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
+        private bool ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
         {
-            Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, bill, distribution });
+            return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, bill, distribution }) != false;
         }
 
-        private List<BillTimberlineHeader> DoProcess(IDataModel<Bill> model)
+        private BillTimberlineResult DoProcess(IDataModel<Bill> model)
         {
-            var apifs = new List<BillTimberlineHeader>();
+            var result = new BillTimberlineResult();
 
             var lines = model.GetTable<BillLine>("Bill_BillLine").ToObjects<BillLine>()
                 .GroupBy(x => x.BillLink.ID).ToDictionary(x => x.Key, x => x.ToList());
@@ -346,66 +350,86 @@ public class Module
                     // SmryPayeeState
                     // SmryPayeeZip
                 };
-                ProcessHeader(model, bill, apif);
-
-                foreach (var billLine in lines.GetValueOrDefault(bill.ID) ?? Enumerable.Empty<BillLine>())
+                if (!ProcessHeader(model, bill, apif))
                 {
-                    var apdf = new BillTimberlineDistribution
+                    result.AddFailed(bill, "Failed by script.");
+                }
+                else
+                {
+                    var success = true;
+                    var billLines = lines.GetValueOrDefault(bill.ID) ?? new List<BillLine>();
+                    foreach (var billLine in billLines)
                     {
-                        // Equipment
-                        // EQ Cost Code
-                        // Extra
-                        // Cost Code
-                        // Category
-                        /// BL STd Item
-                        // Reserved
-                        // Expense account
-                        // AP Account
-                        // Taxable Payments
-                        TaxGroup = billLine.TaxCode.Code,
-                        Amount = billLine.IncTax,
-                        Tax = billLine.Tax,
-                        // Tax Liability
-                        // Discount OFfered
-                        // Retainage
-                        // MIsc Deduction
-                        // Tax Payments Exempt
-                        // Dist Code
-                        // Misc Entry 1
-                        // Misc Units 1
-                        // Misc Entry 2
-                        // Misc Units 2
-                        // Meter
-                        Description = billLine.Description,
-                        // Authorization
-                        // Joint Payee
-                    };
-                    if(purchaseOrderItems.TryGetValue(billLine.OrderItem.ID, out var poItem))
+                        var apdf = new BillTimberlineDistribution
+                        {
+                            // Equipment
+                            // EQ Cost Code
+                            // Extra
+                            // Cost Code
+                            // Category
+                            /// BL STd Item
+                            // Reserved
+                            // Expense account
+                            // AP Account
+                            // Taxable Payments
+                            TaxGroup = billLine.TaxCode.Code,
+                            Amount = billLine.IncTax,
+                            Tax = billLine.Tax,
+                            // Tax Liability
+                            // Discount OFfered
+                            // Retainage
+                            // MIsc Deduction
+                            // Tax Payments Exempt
+                            // Dist Code
+                            // Misc Entry 1
+                            // Misc Units 1
+                            // Misc Entry 2
+                            // Misc Units 2
+                            // Meter
+                            Description = billLine.Description,
+                            // Authorization
+                            // Joint Payee
+                        };
+                        if (purchaseOrderItems.TryGetValue(billLine.OrderItem.ID, out var poItem))
+                        {
+                            apdf.Commitment = poItem.PONumber;
+                            apdf.Job = poItem.Job.JobNumber;
+                            if (int.TryParse(poItem.ReceivedReference, out var itemNumber))
+                            {
+                                apdf.CommitmentLineItem = itemNumber;
+                                billLine.PostedReference = poItem.ReceivedReference;
+                            }
+                            apdf.Units = poItem.Qty;
+                            apdf.UnitCost = poItem.Cost;
+                        }
+
+                        if (!ProcessLine(model, billLine, apdf))
+                        {
+                            success = false;
+                            break;
+                        }
+                        apif.Distributions.Add(apdf);
+                    }
+                    if (success)
                     {
-                        apdf.Commitment = poItem.PONumber;
-                        apdf.Job = poItem.Job.JobNumber;
-                        if (int.TryParse(poItem.ReceivedReference, out var itemNumber))
+                        foreach(var billLine in billLines)
                         {
-                            apdf.CommitmentLineItem = itemNumber;
-                            billLine.PostedReference = poItem.ReceivedReference;
+                            result.AddFragment(billLine);
                         }
-                        apdf.Units = poItem.Qty;
-                        apdf.UnitCost = poItem.Cost;
+                        result.AddSuccess(bill, apif);
+                    }
+                    else
+                    {
+                        result.AddFailed(bill, "Failed by script.");
                     }
-
-                    ProcessLine(model, billLine, apdf);
-                    apif.Distributions.Add(apdf);
-
-                    AddFragment?.Invoke(billLine);
                 }
-                apifs.Add(apif);
             }
-            return apifs;
+            return result;
         }
 
-        public bool Process(IDataModel<Bill> model)
+        public IPostResult<Bill> Process(IDataModel<Bill> model)
         {
-            var apifs = DoProcess(model);
+            var result = DoProcess(model);
 
             var dlg = new SaveFileDialog()
             {
@@ -416,7 +440,7 @@ public class Module
             {
                 using var writer = new StreamWriter(dlg.FileName);
                 using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
-                foreach(var apif in apifs)
+                foreach(var apif in result.Exports)
                 {
                     csv.WriteRecord(apif);
                     csv.NextRecord();
@@ -426,14 +450,14 @@ public class Module
                         csv.NextRecord();
                     }
                 }
-                return true;
+                return result;
             }
             else
             {
                 throw new PostCancelledException();
             }
         }
-        public void AfterPost(IDataModel<Bill> model)
+        public void AfterPost(IDataModel<Bill> model, IPostResult<Bill> result)
         {
             Script?.Execute(methodname: "AfterPost", parameters: new object[] { model });
         }

+ 113 - 66
prs.shared/Posters/Timberline/PurchaseOrderTimberlinePoster.cs

@@ -187,12 +187,14 @@ public class Module
 
     public void ProcessHeader(IDataModel<PurchaseOrder> model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header)
     {
-        // Do extra processing for a purchase order
+        // Do extra processing for a purchase order; return false to fail this purchase order
+        return true;
     }
 
     public void ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
     {
-        // Do extra processing for a purchase order line
+        // Do extra processing for a purchase order line; return false to fail this purchase order
+        return true;
     }
 
     public void AfterPost(IDataModel<PurchaseOrder> model)
@@ -203,13 +205,15 @@ public class Module
         }
     }
 
+    public class PurchaseOrderTimberlineResult : TimberlinePostResult<PurchaseOrderTimberlineHeader, PurchaseOrder>
+    {
+    }
+
     public class PurchaseOrderTimberlinePoster : ITimberlinePoster<PurchaseOrder, PurchaseOrderTimberlineSettings>
     {
         public ScriptDocument? Script { get; set; }
         public PurchaseOrderTimberlineSettings Settings { get; set; }
 
-        public event ITimberlinePoster<PurchaseOrder, PurchaseOrderTimberlineSettings>.AddFragmentCallback? AddFragment;
-
         public bool BeforePost(IDataModel<PurchaseOrder> model)
         {
             model.SetIsDefault<Document>(false, alias: "CompanyLogo");
@@ -241,20 +245,18 @@ public class Module
             return true;
         }
 
-        private void ProcessHeader(IDataModel<PurchaseOrder> model, PurchaseOrder bill, PurchaseOrderTimberlineHeader header)
+        private bool ProcessHeader(IDataModel<PurchaseOrder> model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header)
         {
-            Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, bill, header });
+            return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, purchaseOrder, header }) != false;
         }
-        private void ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
+        private bool ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
         {
-            Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, purchaseOrderItem, line });
+            return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, purchaseOrderItem, line }) != false;
         }
 
-        private List<PurchaseOrderTimberlineHeader> DoProcess(IDataModel<PurchaseOrder> model)
+        private PurchaseOrderTimberlineResult DoProcess(IDataModel<PurchaseOrder> model)
         {
-            List<IPostableFragment> Fragments = new List<IPostableFragment>();
-
-            var cs = new List<PurchaseOrderTimberlineHeader>();
+            var cs = new PurchaseOrderTimberlineResult();
 
             var lines = model.GetTable<PurchaseOrderItem>("PurchaseOrder_PurchaseOrderItem").ToObjects<PurchaseOrderItem>()
                 .GroupBy(x => x.PurchaseOrderLink.ID).ToDictionary(x => x.Key, x => x.ToList());
@@ -272,73 +274,92 @@ public class Module
                     Closed = purchaseOrder.ClosedDate != DateTime.MinValue,
                     // Printed
                 };
-                ProcessHeader(model, purchaseOrder, c);
-
-                // Dictionary from line number to POItem.
-                var items = new Dictionary<int, PurchaseOrderItem>();
-                var POItems = lines.GetValueOrDefault(purchaseOrder.ID)?.ToList() ?? new List<PurchaseOrderItem>();
-                foreach (var purchaseOrderItem in POItems)
+                if(!ProcessHeader(model, purchaseOrder, c))
+                {
+                    cs.AddFailed(purchaseOrder, "Failed by script.");
+                }
+                else
                 {
-                    if(int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
+                    // Dictionary from line number to POItem.
+                    var items = new Dictionary<int, PurchaseOrderItem>();
+                    var POItems = lines.GetValueOrDefault(purchaseOrder.ID)?.ToList() ?? new List<PurchaseOrderItem>();
+                    foreach (var purchaseOrderItem in POItems)
                     {
-                        if (items.TryGetValue(itemNumber, out var oldItem))
+                        if (int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
                         {
-                            // Theoretically shouldn't happen, but just in case.
-                            MessageBox.Show($"Warning: Multiple PurchaseOrder Items have the same line number for export; the line number for '{purchaseOrderItem.Description}' will be changed in the export.");
-                            Logger.Send(LogType.Error, "", $"Purchase Order Post: Multiple POItems with the same Line Number; changing line number of POItem {purchaseOrderItem.ID}");
-                            purchaseOrderItem.PostedReference = "";
+                            if (items.TryGetValue(itemNumber, out var oldItem))
+                            {
+                                // Theoretically shouldn't happen, but just in case.
+                                MessageBox.Show($"Warning: Multiple PurchaseOrder Items have the same line number for export; the line number for '{purchaseOrderItem.Description}' will be changed in the export.");
+                                Logger.Send(LogType.Error, "", $"Purchase Order Post: Multiple POItems with the same Line Number; changing line number of POItem {purchaseOrderItem.ID}");
+                                purchaseOrderItem.PostedReference = "";
+                            }
+                            else
+                            {
+                                items[itemNumber] = purchaseOrderItem;
+                            }
                         }
-                        else
+                    }
+
+                    var success = true;
+                    foreach (var purchaseOrderItem in POItems)
+                    {
+                        if (!int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
                         {
+                            itemNumber = 1;
+                            while (items.ContainsKey(itemNumber))
+                            {
+                                ++itemNumber;
+                            }
+
                             items[itemNumber] = purchaseOrderItem;
+                            purchaseOrderItem.PostedReference = itemNumber.ToString();
                         }
+                        var ci = new PurchaseOrderTimberlineLine
+                        {
+                            CommitmentID = purchaseOrder.PONumber,
+                            ItemNumber = itemNumber,
+                            Description = purchaseOrderItem.Description,
+                            // RetainagePercent = ,
+                            DeliveryDate = purchaseOrderItem.ReceivedDate,
+                            //ScopeOfWork
+                            Job = purchaseOrderItem.Job.JobNumber,
+                            //Extra = purchaseOrderItem.Job
+                            CostCode = purchaseOrderItem.CostCentre.Code,
+                            //Category = purchaseOrderItem.cat
+                            TaxGroup = purchaseOrderItem.TaxCode.Code,
+                            Units = purchaseOrderItem.Qty,
+                            UnitCost = purchaseOrderItem.Cost,
+                            UnitDescription = purchaseOrderItem.Dimensions.UnitSize,
+                            Amount = purchaseOrderItem.IncTax,
+                            // BoughtOut
+                        };
+
+                        if(!ProcessLine(model, purchaseOrderItem, ci))
+                        {
+                            success = false;
+                            break;
+                        }
+                        c.Lines.Add(ci);
                     }
-                }
-
-                foreach (var purchaseOrderItem in POItems)
-                {
-                    if (!int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
+                    if (success)
                     {
-                        itemNumber = 1;
-                        while(items.ContainsKey(itemNumber))
+                        foreach(var item in POItems)
                         {
-                            ++itemNumber;
+                            cs.AddFragment(item);
                         }
-
-                        items[itemNumber] = purchaseOrderItem;
-                        purchaseOrderItem.PostedReference = itemNumber.ToString();
+                        cs.AddSuccess(purchaseOrder, c);
                     }
-                    var ci = new PurchaseOrderTimberlineLine
+                    else
                     {
-                        CommitmentID = purchaseOrder.PONumber,
-                        ItemNumber = itemNumber,
-                        Description = purchaseOrderItem.Description,
-                        // RetainagePercent = ,
-                        DeliveryDate = purchaseOrderItem.ReceivedDate,
-                        //ScopeOfWork
-                        Job = purchaseOrderItem.Job.JobNumber,
-                        //Extra = purchaseOrderItem.Job
-                        CostCode = purchaseOrderItem.CostCentre.Code,
-                        //Category = purchaseOrderItem.cat
-                        TaxGroup = purchaseOrderItem.TaxCode.Code,
-                        Units = purchaseOrderItem.Qty,
-                        UnitCost = purchaseOrderItem.Cost,
-                        UnitDescription = purchaseOrderItem.Dimensions.UnitSize,
-                        Amount = purchaseOrderItem.IncTax,
-                        // BoughtOut
-                    };
-
-                    ProcessLine(model, purchaseOrderItem, ci);
-                    c.Lines.Add(ci);
-
-                    AddFragment?.Invoke(purchaseOrderItem);
+                        cs.AddFailed(purchaseOrder, "Failed by script.");
+                    }
                 }
-                cs.Add(c);
             }
             return cs;
         }
 
-        public bool Process(IDataModel<PurchaseOrder> model)
+        public IPostResult<PurchaseOrder> Process(IDataModel<PurchaseOrder> model)
         {
             var POs = DoProcess(model);
 
@@ -352,7 +373,7 @@ public class Module
                 using (var writer = new StreamWriter(dlg.FileName))
                 {
                     using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
-                    foreach (var header in POs)
+                    foreach (var header in POs.Exports)
                     {
                         // Write the record.
                         csv.WriteRecord(header);
@@ -414,23 +435,49 @@ public class Module
                             var id = csv.GetField(0);
                             if(id == "C")
                             {
-                                rejectedHeaders.Add(csv.GetRecord<PurchaseOrderTimberlineHeader>());
+                                var header = csv.GetRecord<PurchaseOrderTimberlineHeader>();
+                                if(header is not null)
+                                {
+                                    var entry = POs.Items.FirstOrDefault(x => x.Item2?.CommitmentID.Equals(header.CommitmentID) == true);
+                                    if(entry is not null)
+                                    {
+                                        (entry.Item1 as IPostable).FailPost("");
+                                    }
+                                }
+                                else
+                                {
+                                    Logger.Send(LogType.Error, "", "PO Timberline export: Unable to parse header from CSV line in rejection file.");
+                                    MessageBox.Show("Invalid line in file; skipping.");
+                                }
                             }
                             else if(id == "CI")
                             {
-                                rejectedLines.Add(csv.GetRecord<PurchaseOrderTimberlineLine>());
+                                var line = csv.GetRecord<PurchaseOrderTimberlineLine>();
+                                if (line is not null)
+                                {
+                                    var entry = POs.Items.FirstOrDefault(x => x.Item2?.CommitmentID.Equals(line.CommitmentID) == true);
+                                    if (entry is not null)
+                                    {
+                                        (entry.Item1 as IPostable).FailPost("");
+                                    }
+                                }
+                                else
+                                {
+                                    Logger.Send(LogType.Error, "", "PO Timberline export: Unable to parse line from CSV line in rejection file.");
+                                    MessageBox.Show("Invalid line in file; skipping.");
+                                }
                             }
                         }
                     }
                 }
-                return true;
+                return POs;
             }
             else
             {
                 throw new PostCancelledException();
             }
         }
-        public void AfterPost(IDataModel<PurchaseOrder> model)
+        public void AfterPost(IDataModel<PurchaseOrder> model, IPostResult<PurchaseOrder> result)
         {
             Script?.Execute(methodname: "AfterPost", parameters: new object[] { model });
         }

+ 78 - 17
prs.shared/Posters/Timberline/TimesheetTimberlinePoster.cs

@@ -91,6 +91,8 @@ namespace PRS.Shared
             TimeSpan Duration { get; set; }
 
             string PayrollID { get; set; }
+
+            TimeSheet TimeSheet { get; set; }
         }
 
         public class PaidWorkBlock : IBlock
@@ -105,13 +107,16 @@ namespace PRS.Shared
 
             public string PayrollID { get; set; }
 
-            public PaidWorkBlock(string taskID, TimeSpan duration, string payID, string job)
+            public TimeSheet TimeSheet { get; set; }
+
+            public PaidWorkBlock(string taskID, TimeSpan duration, string payID, string job, TimeSheet timeSheet)
             {
                 TaskID = taskID;
                 Duration = duration;
                 PayrollID = payID;
                 Job = job;
                 Extra = "";
+                TimeSheet = timeSheet;
             }
         }
 
@@ -127,13 +132,16 @@ namespace PRS.Shared
 
             public string PayrollID { get; set; }
 
-            public LeaveBlock(string payrollID, TimeSpan duration)
+            public TimeSheet TimeSheet { get; set; }
+
+            public LeaveBlock(string payrollID, TimeSpan duration, TimeSheet timeSheet)
             {
                 PayrollID = payrollID;
                 Duration = duration;
                 Job = "";
                 Extra = "";
                 TaskID = "";
+                TimeSheet = timeSheet;
             }
         }
 
@@ -204,6 +212,18 @@ namespace PRS.Shared
         }
     }
 
+    public class TimeSheetTimberlineResult : PostResult<TimeSheet>
+    {
+        private List<TimesheetTimberlineItem> items = new List<TimesheetTimberlineItem>();
+
+        public IEnumerable<TimesheetTimberlineItem> Items => items;
+
+        public void AddItem(TimesheetTimberlineItem item)
+        {
+            items.Add(item);
+        }
+    }
+
     public class TimesheetTimberlineItem
     {
         [Index(0)]
@@ -290,8 +310,6 @@ public class Module
         public ScriptDocument? Script { get; set; }
         public TimesheetTimberlineSettings Settings { get; set; }
 
-        public event ITimberlinePoster<TimeSheet, TimesheetTimberlineSettings>.AddFragmentCallback? AddFragment;
-
         private Dictionary<Guid, Activity> _activities = null!; // Initialised on DoProcess()
         private Dictionary<Guid, OvertimeInterval[]> _overtimeIntervals = null!; // Initialised on DoProcess()
 
@@ -474,7 +492,7 @@ public class Module
                                 {
                                     if (interval.IsPaid)
                                     {
-                                        workItems.Add(new(block.TaskID, interval.Interval, interval.PayrollID, block.Job));
+                                        workItems.Add(new(block.TaskID, interval.Interval, interval.PayrollID, block.Job, block.TimeSheet));
                                     }
                                     overtimeIntervals.RemoveAt(overtimeIntervals.Count - 1);
                                     duration -= interval.Interval;
@@ -483,7 +501,7 @@ public class Module
                                 {
                                     if (interval.IsPaid)
                                     {
-                                        workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job));
+                                        workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
                                     }
                                     interval.Interval -= duration;
                                     duration = TimeSpan.Zero;
@@ -492,7 +510,7 @@ public class Module
                             case OvertimeIntervalType.RemainingTime:
                                 if (interval.IsPaid)
                                 {
-                                    workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job));
+                                    workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
                                 }
                                 duration = TimeSpan.Zero;
                                 break;
@@ -502,7 +520,7 @@ public class Module
                     }
                     else
                     {
-                        workItems.Add(new(block.TaskID, duration, "", block.Job));
+                        workItems.Add(new(block.TaskID, duration, "", block.Job, block.TimeSheet));
                         duration = TimeSpan.Zero;
                     }
                 }
@@ -510,9 +528,9 @@ public class Module
             return workItems;
         }
 
-        private List<TimesheetTimberlineItem> DoProcess(IDataModel<TimeSheet> model)
+        private TimeSheetTimberlineResult DoProcess(IDataModel<TimeSheet> model)
         {
-            var items = new List<TimesheetTimberlineItem>();
+            var items = new TimeSheetTimberlineResult();
 
             var timesheets = model.GetTable<TimeSheet>().ToObjects<TimeSheet>().ToList();
             if(timesheets.Any(x => x.Approved.IsEmpty()))
@@ -546,6 +564,10 @@ public class Module
                 ProcessRawData(rawArgs);
                 if (rawArgs.Cancel)
                 {
+                    foreach(var sheet in sheets)
+                    {
+                        items.AddFailed(sheet, "Post cancelled by script.");
+                    }
                     continue;
                 }
 
@@ -554,6 +576,10 @@ public class Module
                 ProcessActivityBlocks(activityArgs);
                 if (activityArgs.Cancel)
                 {
+                    foreach (var sheet in sheets)
+                    {
+                        items.AddFailed(sheet, "Post cancelled by script.");
+                    }
                     continue;
                 }
 
@@ -585,12 +611,12 @@ public class Module
 
                     if (isLeave)
                     {
-                        leave.Add(new(payID, block.Finish - block.Start));
+                        leave.Add(new(payID, block.Finish - block.Start, block.TimeSheet));
                     }
                     else
                     {
                         // Leave PayID blank until we've worked out the rosters
-                        workTime.Add(new(payID, block.Finish - block.Start, "", block.TimeSheet.JobLink.JobNumber));
+                        workTime.Add(new(payID, block.Finish - block.Start, "", block.TimeSheet.JobLink.JobNumber, block.TimeSheet));
                     }
                 }
 
@@ -606,10 +632,22 @@ public class Module
                     ProcessTimeBlocks(blockArgs);
                     if (blockArgs.Cancel)
                     {
+                        foreach (var sheet in sheets)
+                        {
+                            items.AddFailed(sheet, "Post cancelled by script.");
+                        }
                         continue;
                     }
 
+                    // Succeed all sheets, and then fail them if any of their blocks are failed.
+                    foreach (var sheet in sheets)
+                    {
+                        items.AddSuccess(sheet);
+                    }
+
                     var blocks = (blockArgs.WorkBlocks as IEnumerable<IBlock>).Concat(blockArgs.LeaveBlocks);
+
+                    var newItems = new List<Tuple<TimesheetTimberlineItem, List<TimeSheet>>>();
                     foreach(var block in blocks.GroupBy(x => new { x.Job, x.TaskID, x.PayrollID }, x => x))
                     {
                         var item = new TimesheetTimberlineItem
@@ -624,18 +662,41 @@ public class Module
                         };
                         var itemArgs = new ProcessItemArgs(model, key.Employee, key.Date, item);
                         ProcessItem(itemArgs);
+
+                        var blockTimeSheets = block.Select(x => x.TimeSheet).ToList();
                         if (!itemArgs.Cancel)
                         {
-                            items.Add(itemArgs.Item);
+                            newItems.Add(new(itemArgs.Item, blockTimeSheets));
+                        }
+                        else
+                        {
+                            foreach(var sheet in blockTimeSheets)
+                            {
+                                (sheet as IPostable).FailPost("Post cancelled by script.");
+                            }
                         }
                     }
+                    foreach(var item in newItems)
+                    {
+                        if(item.Item2.All(x => x.PostedStatus == PostedStatus.Posted))
+                        {
+                            items.AddItem(item.Item1);
+                        }
+                    }
+                }
+                else
+                {
+                    foreach (var sheet in sheets)
+                    {
+                        items.AddFailed(sheet, "Zero Approved Duration");
+                    }
                 }
             }
 
             return items;
         }
 
-        public bool Process(IDataModel<TimeSheet> model)
+        public IPostResult<TimeSheet> Process(IDataModel<TimeSheet> model)
         {
             var items = DoProcess(model);
 
@@ -648,19 +709,19 @@ public class Module
             {
                 using var writer = new StreamWriter(dlg.FileName);
                 using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
-                foreach (var item in items)
+                foreach (var item in items.Items)
                 {
                     csv.WriteRecord(item);
                     csv.NextRecord();
                 }
-                return true;
+                return items;
             }
             else
             {
                 throw new PostCancelledException();
             }
         }
-        public void AfterPost(IDataModel<TimeSheet> model)
+        public void AfterPost(IDataModel<TimeSheet> model, IPostResult<TimeSheet> result)
         {
             Script?.Execute(methodname: "AfterPost", parameters: new object[] { model });
         }