|  | @@ -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 });
 | 
	
		
			
				|  |  |          }
 |