Explorar el Código

Updates to timberline posters to use new POIA system.

Kenric Nugteren hace 11 meses
padre
commit
025eb97685

+ 8 - 0
prs.classes/EnclosedEntities/Dimensions/Dimensions.cs

@@ -453,5 +453,13 @@ namespace Comal.Classes
             result = default(T);
             return true;
         }
+
+        public static T Copy<T>(this T dim)
+            where T : IDimensions, new()
+        {
+            var newDim = new T();
+            newDim.CopyFrom(dim);
+            return newDim;
+        }
     }
 }

+ 1 - 0
prs.classes/Entities/Job/Job.cs

@@ -8,6 +8,7 @@ namespace Comal.Classes
 {
     public interface IJob : IEntity
     {
+        public string JobNumber { get; set; }
     }
 
     [UserTracking("Project Management")]

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

@@ -259,7 +259,6 @@ namespace Comal.Classes
             {
                 bChanging = false;
             }
-            
         }
     }
 }

+ 4 - 1
prs.classes/Entities/PurchaseOrder/PurchaseOrderItemAllocation.cs

@@ -36,7 +36,7 @@ namespace Comal.Classes
     /// </summary>
     [Caption("Allocation")]
     public class PurchaseOrderItemAllocation : Entity, IRemotable, IPersistent, ILicense<ProjectManagementLicense>
-    , IOneToMany<JobRequisitionItem>, IOneToMany<Job>, IOneToMany<PurchaseOrderItem>
+    , IOneToMany<JobRequisitionItem>, IOneToMany<Job>, IOneToMany<PurchaseOrderItem>, IPostableFragment<PurchaseOrder>
     {
         [EntityRelationship(DeleteAction.Cascade)]
         public PurchaseOrderItemLink Item { get; set; }
@@ -96,6 +96,9 @@ namespace Comal.Classes
         [EditorSequence(3)]
         public double Quantity { get; set; }
 
+        [NullEditor]
+        public string PostedReference { get; set; }
+
         protected override void DoPropertyChanged(string name, object? before, object? after)
         {
             base.DoPropertyChanged(name, before, after);

+ 38 - 0
prs.desktop/Panels/PurchaseOrders/SupplierPurchaseOrderItemOneToMany.cs

@@ -145,6 +145,44 @@ public class SupplierPurchaseOrderItemOneToMany : DynamicOneToManyGrid<PurchaseO
         assignLocation = AddButton("Assign Location", null, AssignLocation);
     }
 
+    public override DynamicGridColumns GenerateColumns()
+    {
+        if (IsDirectEditMode())
+        {
+            var columns = new DynamicGridColumns();
+            columns.Add<PurchaseOrderItem, string>(x => x.Description);
+            columns.Add<PurchaseOrderItem, Guid>(x => x.Product.ID, caption: "Product", width: 100);
+            columns.Add<PurchaseOrderItem, Guid>(x => x.Style.ID, caption: "Style", width: 100);
+            columns.Add<PurchaseOrderItem, string>(x => x.Dimensions.UnitSize, width: 70, caption: "Size");
+            columns.Add<PurchaseOrderItem, Guid>(x => x.Job.ID, caption: "Job", width: 70);
+            columns.Add<PurchaseOrderItem, double>(x => x.Qty, width: 50);
+            columns.Add<PurchaseOrderItem, double>(x => x.Cost);
+            columns.Add<PurchaseOrderItem, double>(x => x.ExTax);
+            columns.Add<PurchaseOrderItem, Guid>(x => x.TaxCode.ID, caption: "Tax", width: 50);
+            columns.Add<PurchaseOrderItem, Guid>(x => x.CostCentre.ID, width: 70);
+            columns.Add<PurchaseOrderItem, Guid>(x => x.PurchaseGL.ID, width: 70);
+            columns.Add<PurchaseOrderItem, DateTime>(x => x.ReceivedDate, width: 100);
+            return columns;
+        }
+        else
+        {
+            var columns = new DynamicGridColumns();
+            columns.Add<PurchaseOrderItem, string>(x => x.Description);
+            columns.Add<PurchaseOrderItem, string>(x => x.Product.Code, caption: "Product", width: 100);
+            columns.Add<PurchaseOrderItem, string>(x => x.Style.Code, caption: "Style", width: 100);
+            columns.Add<PurchaseOrderItem, string>(x => x.Dimensions.UnitSize, width: 70, caption: "Size");
+            columns.Add<PurchaseOrderItem, string>(x => x.Job.JobNumber, caption: "Job", width: 70);
+            columns.Add<PurchaseOrderItem, double>(x => x.Qty, width: 50);
+            columns.Add<PurchaseOrderItem, double>(x => x.Cost);
+            columns.Add<PurchaseOrderItem, double>(x => x.ExTax);
+            columns.Add<PurchaseOrderItem, string>(x => x.TaxCode.Code, caption: "Tax", width: 50);
+            columns.Add<PurchaseOrderItem, string>(x => x.CostCentre.Code, width: 70);
+            columns.Add<PurchaseOrderItem, string>(x => x.PurchaseGL.Code, width: 70);
+            columns.Add<PurchaseOrderItem, DateTime>(x => x.ReceivedDate, width: 100);
+            return columns;
+        }
+    }
+
     protected override void DoReconfigure(DynamicGridOptions options)
     {
         base.DoReconfigure(options);

+ 15 - 35
prs.desktop/Panels/Suppliers/Bills/SupplierBillLineGrid.cs

@@ -554,44 +554,24 @@ public class SupplierBillLineGrid : DynamicOneToManyGrid<Bill, BillLine>
             CreateItems(() =>
             {
                 List<BillLine> lines = new List<BillLine>();
-                foreach (var row in items.Rows)
+                foreach (var poItem in items.ToObjects<PurchaseOrderItem>())
                 {
-
                     var line = CreateItem();
 
-                    line.OrderItem.ID = row.Get<PurchaseOrderItem, Guid>(x => x.ID);
-                    line.OrderItem.PurchaseOrderLink.ID = row.Get<PurchaseOrderItem, Guid>(x => x.PurchaseOrderLink.ID);
-                    line.OrderItem.PurchaseOrderLink.PONumber =
-                        row.Get<PurchaseOrderItem, String>(x => x.PurchaseOrderLink.PONumber);
-
-                    line.OrderItem.Product.ID = row.Get<PurchaseOrderItem, Guid>(x => x.Product.ID);
-                    line.OrderItem.Product.Code = row.Get<PurchaseOrderItem, string>(x => x.Product.Code);
-                    line.OrderItem.Product.Name = row.Get<PurchaseOrderItem, string>(x => x.Product.Name);
-
-                    line.OrderItem.Description = row.Get<PurchaseOrderItem, string>(x => x.Description);
-                    line.OrderItem.Qty = row.Get<PurchaseOrderItem, double>(x => x.Qty);
-                    line.ForeignCurrencyCost = row.Get<PurchaseOrderItem, double>(x => x.ForeignCurrencyCost);
-                    line.OrderItem.ExTax = row.Get<PurchaseOrderItem, double>(x => x.ExTax);
-                    line.OrderItem.TaxCode.ID = row.Get<PurchaseOrderItem, Guid>(x => x.TaxCode.ID);
-                    line.OrderItem.TaxCode.Code = row.Get<PurchaseOrderItem, string>(x => x.TaxCode.Code);
-                    line.OrderItem.TaxCode.Description = row.Get<PurchaseOrderItem, string>(x => x.TaxCode.Description);
-                    line.OrderItem.TaxCode.Rate = row.Get<PurchaseOrderItem, double>(x => x.TaxCode.Rate);
-                    line.OrderItem.PurchaseGL.ID = row.Get<PurchaseOrderItem, Guid>(x => x.PurchaseGL.ID);
-                    line.OrderItem.CostCentre.ID = row.Get<PurchaseOrderItem, Guid>(x => x.CostCentre.ID);
-
-                    line.ExTax = row.Get<PurchaseOrderItem, double>(x => x.ExTax);
-                    line.TaxCode.ID = row.Get<PurchaseOrderItem, Guid>(x => x.TaxCode.ID);
-                    line.TaxCode.Code = row.Get<PurchaseOrderItem, string>(x => x.TaxCode.Code);
-                    line.TaxCode.Description = row.Get<PurchaseOrderItem, string>(x => x.TaxCode.Description);
-                    line.TaxCode.Rate = row.Get<PurchaseOrderItem, double>(x => x.TaxCode.Rate);
-                    line.TaxRate = row.Get<PurchaseOrderItem, double>(x => x.TaxRate);
-                    line.Tax = row.Get<PurchaseOrderItem, double>(x => x.Tax);
-                    line.IncTax = row.Get<PurchaseOrderItem, double>(x => x.IncTax);
-                    line.PurchaseGL.ID = row.Get<PurchaseOrderItem, Guid>(x => x.PurchaseGL.ID);
-                    line.CostCentre.ID = row.Get<PurchaseOrderItem, Guid>(x => x.CostCentre.ID);
-                    var description = row.Get<PurchaseOrderItem, String>(x => x.Description);
-                    if (String.IsNullOrWhiteSpace(description))
-                        description = row.Get<PurchaseOrderItem, String>(x => x.Product.Name);
+                    line.OrderItem.CopyFrom(poItem);
+                    line.ForeignCurrencyCost = poItem.ForeignCurrencyCost;
+
+                    line.ExTax = poItem.ExTax;
+                    line.TaxCode.CopyFrom(poItem.TaxCode);
+                    line.TaxRate = poItem.TaxRate;
+                    line.Tax = poItem.Tax;
+                    line.IncTax = poItem.IncTax;
+                    line.PurchaseGL.ID = poItem.PurchaseGL.ID;
+                    line.CostCentre.ID = poItem.CostCentre.ID;
+
+                    var description = poItem.Description;
+                    if (description.IsNullOrWhiteSpace())
+                        description = poItem.Product.Name;
                     line.Description = description; //$"{row.Get<PurchaseOrderItem, double>(x => x.Qty):F2} x {description}";
 
                     lines.Add(line);

+ 125 - 48
prs.shared/Posters/Timberline/BillTimberlinePoster.cs

@@ -3,12 +3,14 @@ using CsvHelper;
 using CsvHelper.Configuration;
 using CsvHelper.Configuration.Attributes;
 using FastReport.Utils;
+using InABox.Clients;
 using InABox.Core;
 using InABox.Core.Postable;
 using InABox.Poster.Timberline;
 using InABox.Scripting;
 using Microsoft.Win32;
 using NPOI.SS.Formula.Functions;
+using PRSDimensionUtils;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -261,9 +263,11 @@ public class Module
         return true;
     }
 
-    public bool ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
+    public bool ProcessLine(IDataModel<Bill> model, BillLine bill, PurchaseOrderItem? item, PurchaseOrderItemAllocation? allocation, BillTimberlineDistribution distribution)
     {
-        // Do extra processing for a distribution line; return false to fail this header
+        // Do extra processing for a distribution line; return false to fail this header. The BillLine will be split based on the allocations on the purchase order,
+        // so that information is given here to with 'item' and 'allocation'; these are both 'null' if the bill isn't linked to a PurchaseOrderItem,
+        // and 'allocation' is 'null' if the line corresponds to the primary allocation, based on the job number of the PurchaseOrderItem.
         return true;
     }
 
@@ -307,6 +311,7 @@ public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettin
         model.SetColumns(Columns.None<BillLine>().Add(x => x.ID)
             .Add(x => x.BillLink.ID)
             .Add(x => x.TaxCode.Code)
+            .Add(x => x.CostCentre.Code)
             .Add(x => x.IncTax)
             .Add(x => x.Tax)
             .Add(x => x.Description)
@@ -322,9 +327,20 @@ public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettin
                 .Add(x => x.Qty)
                 .Add(x => x.Description)
                 .Add(x => x.Cost)
+                .Add(x => x.CostCentre.Code)
+                .Add(x => x.PurchaseGL.Code)
                 .Add(x => x.PostedReference)
+                .Add(x => x.Job.ID)
                 .Add(x => x.Job.JobNumber)
-                );
+                .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local));
+        model.AddChildTable<PurchaseOrderItem, PurchaseOrderItemAllocation>(x => x.ID, x => x.Item.ID, isdefault: true,
+            parentalias: "POItem", childalias: "Allocations",
+            columns: Columns.None<PurchaseOrderItemAllocation>()
+                .Add(x => x.Item.ID)
+                .Add(x => x.Quantity)
+                .Add(x => x.PostedReference)
+                .Add(x => x.Job.ID)
+                .Add(x => x.Job.JobNumber));
 
         Script?.Execute(methodname: "BeforePost", parameters: new object[] { model });
         return true;
@@ -334,9 +350,22 @@ public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettin
     {
         return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, bill, header }) != false;
     }
-    private bool ProcessLine(IDataModel<Bill> model, BillLine bill, BillTimberlineDistribution distribution)
+    private bool ProcessLine(IDataModel<Bill> model, BillLine bill, PurchaseOrderItem? item, PurchaseOrderItemAllocation? allocation, BillTimberlineDistribution distribution)
+    {
+        return Script?.Execute(methodname: "ProcessLine", parameters: new object?[] { model, bill, item, allocation, distribution }) != false;
+    }
+
+    private class LineData(IJob job, double poCost, double poQty, IPostableFragment<PurchaseOrder> item, double qty)
     {
-        return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, bill, distribution }) != false;
+        public IJob Job { get; set; } = job;
+
+        public double POCost { get; set; } = poCost;
+
+        public double POQty { get; set; } = poQty;
+
+        public IPostableFragment<PurchaseOrder> Item { get; set; } = item;
+
+        public double Qty { get; set; } = qty;
     }
 
     private BillTimberlineResult DoProcess(IDataModel<Bill> model)
@@ -347,6 +376,9 @@ public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettin
             .GroupBy(x => x.BillLink.ID).ToDictionary(x => x.Key, x => x.ToList());
         var purchaseOrderItems = model.GetTable<PurchaseOrderItem>("POItem").ToObjects<PurchaseOrderItem>()
             .ToDictionary(x => x.ID, x => x);
+        var allocations = model.GetTable<PurchaseOrderItemAllocation>("Allocations").ToObjects<PurchaseOrderItemAllocation>()
+            .GroupBy(x => x.Item.ID)
+            .ToDictionary(x => x.Key, x => x.ToList());
 
         var bills = model.GetTable<Bill>().ToObjects<Bill>();
         if(bills.Any(x => x.Approved.IsEmpty()))
@@ -395,60 +427,105 @@ public class BillTimberlinePoster : ITimberlinePoster<Bill, BillTimberlineSettin
             var billLines = lines.GetValueOrDefault(bill.ID) ?? new List<BillLine>();
             foreach (var billLine in billLines)
             {
-                var apdf = new BillTimberlineDistribution
+                BillTimberlineDistribution CreateLine()
                 {
-                    // Equipment
-                    // EQ Cost Code
-                    // Extra
-                    // Cost Code
-                    // Category
-                    /// BL STd Item
-                    // Reserved
-                    ExpenseAccount = billLine.PurchaseGL.Code,
-                    // AP Account
-                    // Taxable Payments
-                    TaxGroup = billLine.TaxCode.Code,
-                    Amount = Math.Round(billLine.IncTax, 4),
-                    Tax = Math.Round(billLine.Tax, 4),
-                    // 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
-                };
+                    var apdf = new BillTimberlineDistribution
+                    {
+                        // Equipment
+                        // EQ Cost Code
+                        // Extra
+                        // Cost Code
+                        // Category
+                        /// BL STd Item
+                        // Reserved
+                        ExpenseAccount = billLine.PurchaseGL.Code,
+                        // AP Account
+                        // Taxable Payments
+                        TaxGroup = billLine.TaxCode.Code,
+                        // 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
+                    };
+                    return apdf;
+                }
+
+                var lineData = new List<LineData>();
                 if (purchaseOrderItems.TryGetValue(billLine.OrderItem.ID, out var poItem))
                 {
-                    apdf.Commitment = poItem.PurchaseOrderLink.PONumber;
-                    apdf.Job = poItem.Job.JobNumber;
-                    if (int.TryParse(poItem.PostedReference, out var itemNumber))
+                    var dimensions = poItem.Dimensions.Copy();
+                    var qty = DimensionUtils.ConvertDimensions(dimensions, poItem.Qty, Client<ProductDimensionUnit>.Provider);
+                    var poCost = poItem.Cost;
+                    var poQty = poItem.Qty;
+                    if (!qty.IsEffectivelyEqual(poItem.Qty))
+                    {
+                        poCost = poItem.Cost * poItem.Qty / (qty.IsEffectivelyEqual(0.0) ? 1.0 : qty);
+                        poQty = qty;
+                    }
+
+                    if(allocations.TryGetValue(billLine.OrderItem.ID, out var poias))
+                    {
+                        lineData.AddRange(poias.Select(x => new LineData(x.Job, poCost, poQty, x, x.Quantity)));
+                        lineData.Add(new(poItem.Job, poCost, poQty, poItem, poQty - poias.Sum(x => x.Quantity)));
+                    }
+
+                    foreach(var line in lineData)
                     {
-                        apdf.CommitmentLineItem = itemNumber;
-                        billLine.PostedReference = poItem.PostedReference;
+                        var apdf = CreateLine();
+
+                        apdf.Commitment = poItem.PurchaseOrderLink.PONumber;
+                        apdf.ExpenseAccount = poItem.PurchaseGL.Code;
+                        if(line.Job.ID != Guid.Empty)
+                        {
+                            apdf.Job = line.Job.JobNumber;
+                            apdf.CostCode = poItem.CostCentre.Code;
+                        }
+                        if (int.TryParse(line.Item.PostedReference, out var itemNumber))
+                        {
+                            apdf.CommitmentLineItem = itemNumber;
+                            billLine.PostedReference = line.Item.PostedReference;
+                        }
+                        apdf.Units = Math.Round(line.Qty, 4);
+                        apdf.UnitCost = Math.Round(line.POCost, 4);
+                        apdf.Description = poItem.Description.NotWhiteSpaceOr(apdf.Description);
+
+                        apdf.Tax = Math.Round(billLine.Tax * line.Qty / line.POQty, 4);
+                        apdf.Amount = Math.Round(billLine.IncTax * line.Qty / line.POQty, 4);
+
+                        if (!ProcessLine(model, billLine, poItem, line.Item as PurchaseOrderItemAllocation, apdf))
+                        {
+                            success = false;
+                            break;
+                        }
+                        apif.Distributions.Add(apdf);
                     }
-                    apdf.Units = Math.Round(poItem.Qty, 4);
-                    apdf.UnitCost = Math.Round(poItem.Cost, 4);
-                    apdf.Description = poItem.Description.NotWhiteSpaceOr(apdf.Description);
                 }
                 else
                 {
+                    var apdf = CreateLine();
                     apdf.Job = billLine.Job.JobNumber;
-                }
+                    apdf.CostCode = billLine.CostCentre.Code;
+                    apdf.Amount = Math.Round(billLine.IncTax, 4);
+                    apdf.Tax = Math.Round(billLine.Tax, 4);
 
-                if (!ProcessLine(model, billLine, apdf))
-                {
-                    success = false;
-                    break;
+                    if (!ProcessLine(model, billLine, null, null, apdf))
+                    {
+                        success = false;
+                        break;
+                    }
+                    apif.Distributions.Add(apdf);
                 }
-                apif.Distributions.Add(apdf);
+
             }
             if (success)
             {

+ 114 - 36
prs.shared/Posters/Timberline/PurchaseOrderTimberlinePoster.cs

@@ -16,6 +16,9 @@ using CsvHelper.TypeConversion;
 using CsvHelper.Configuration;
 using System.Reflection;
 using System.Windows;
+using PRSDimensionUtils;
+using InABox.Clients;
+using InABox.Wpf;
 
 namespace PRS.Shared
 {
@@ -190,7 +193,7 @@ public class Module
         return true;
     }
 
-    public void ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
+    public void ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderItemAllocation? allocation, PurchaseOrderTimberlineLine line)
     {
         // Do extra processing for a purchase order line; return false to fail this purchase order
         return true;
@@ -236,11 +239,21 @@ public class Module
                 .Add(x => x.TaxCode.Code)
                 .Add(x => x.Qty)
                 .Add(x => x.Cost)
-                .Add(x => x.Dimensions.UnitSize)
                 .Add(x => x.IncTax)
-                .Add(x => x.Job.JobNumber),
+                .Add(x => x.Job.JobNumber)
+                .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data),
                 alias: "PurchaseOrder_PurchaseOrderItem");
 
+            model.AddChildTable<PurchaseOrderItem, PurchaseOrderItemAllocation>(x => x.ID, x => x.Item.ID,
+                isdefault: true,
+                columns: Columns.None<PurchaseOrderItemAllocation>()
+                    .Add(x => x.ID)
+                    .Add(x => x.PostedReference)
+                    .Add(x => x.Item.ID)
+                    .Add(x => x.Quantity)
+                    .Add(x => x.Job.JobNumber),
+                parentalias: "PurchaseOrder_PurchaseOrderItem", childalias: "Allocations");
+
             Script?.Execute(methodname: "BeforePost", parameters: new object[] { model });
             return true;
         }
@@ -249,9 +262,24 @@ public class Module
         {
             return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, purchaseOrder, header }) != false;
         }
-        private bool ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
+        private bool ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderItemAllocation? allocation, PurchaseOrderTimberlineLine line)
         {
-            return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, purchaseOrderItem, line }) != false;
+            return Script?.Execute(methodname: "ProcessLine", parameters: new object?[] { model, purchaseOrderItem, allocation, line }) != false;
+        }
+
+        private class LineData(PurchaseOrderItem poItem, double poCost, double poQty, StockDimensions poDim, IPostableFragment<PurchaseOrder> item, double qty)
+        {
+            public PurchaseOrderItem POItem { get; set; } = poItem;
+
+            public double POCost { get; set; } = poCost;
+
+            public double POQty { get; set; } = poQty;
+
+            public StockDimensions PODimensions { get; set; } = poDim;
+
+            public IPostableFragment<PurchaseOrder> Item { get; set; } = item;
+
+            public double Qty { get; set; } = qty;
         }
 
         private PurchaseOrderTimberlineResult DoProcess(IDataModel<PurchaseOrder> model)
@@ -260,6 +288,9 @@ public class Module
 
             var lines = model.GetTable<PurchaseOrderItem>("PurchaseOrder_PurchaseOrderItem").ToObjects<PurchaseOrderItem>()
                 .GroupBy(x => x.PurchaseOrderLink.ID).ToDictionary(x => x.Key, x => x.ToList());
+            var allocations = model.GetTable<PurchaseOrderItemAllocation>("Allocations")
+                .ToObjects<PurchaseOrderItemAllocation>()
+                .GroupBy(x => x.Item.ID).ToDictionary(x => x.Key, x => x.ToList());
             foreach (var purchaseOrder in model.GetTable<PurchaseOrder>().ToObjects<PurchaseOrder>())
             {
                 var c = new PurchaseOrderTimberlineHeader
@@ -280,73 +311,120 @@ public class Module
                 }
                 else
                 {
-                    // Dictionary from line number to POItem.
-                    var items = new Dictionary<int, PurchaseOrderItem>();
+                    // Hashset of line numbers.
+                    var items = new HashSet<int>();
                     var POItems = lines.GetValueOrDefault(purchaseOrder.ID)?.ToList() ?? new List<PurchaseOrderItem>();
-                    foreach (var purchaseOrderItem in POItems)
+
+                    var lineData = new List<LineData>();
+
+                    // Get the data for all the lines.
+                    foreach(var poItem in POItems)
                     {
-                        if (int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
+                        // First, we must convert the dimensions and qty on the POItem to match the "exploded" allocations. Note we need to make copies,
+                        // because we will be saving this poItem later on.
+                        var dimensions = poItem.Dimensions.Copy();
+                        var qty = DimensionUtils.ConvertDimensions(dimensions, poItem.Qty, Client<ProductDimensionUnit>.Provider);
+                        var poCost = poItem.Cost;
+                        var poQty = poItem.Qty;
+                        if (!qty.IsEffectivelyEqual(poItem.Qty))
+                        {
+                            poCost = poItem.Cost * poItem.Qty / (qty.IsEffectivelyEqual(0.0) ? 1.0 : qty);
+                            poQty = qty;
+                        }
+
+                        if(!allocations.TryGetValue(poItem.ID, out var poias))
                         {
-                            if (items.TryGetValue(itemNumber, out var oldItem))
+                            poias = new();
+                        }
+                        var remQty = poItem.Qty - poias.Sum(x => x.Quantity);
+
+                        if(remQty != 0)
+                        {
+                            lineData.Add(new(poItem, poCost, poQty, dimensions, poItem, remQty));
+                        }
+                        foreach(var poia in poias)
+                        {
+                            lineData.Add(new(poItem, poCost, poQty, dimensions, poia, poia.Quantity));
+                        }
+                    }
+
+                    // Check line numbers.
+                    foreach(var data in lineData)
+                    {
+                        if (int.TryParse(data.Item.PostedReference, out var itemNumber))
+                        {
+                            if (!items.Add(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 = "";
-                            }
-                            else
-                            {
-                                items[itemNumber] = purchaseOrderItem;
+                                MessageWindow.Warn(
+                                    $"Warning: Multiple PurchaseOrder Items have the same line number for export; the line number for '{data.POItem.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 {(data.Item as Entity)!.ID}");
+
+                                data.Item.PostedReference = "";
                             }
                         }
                     }
 
+                    // Do post.
                     var success = true;
-                    foreach (var purchaseOrderItem in POItems)
+                    foreach(var data in lineData)
                     {
-                        if (!int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
+                        if (!int.TryParse(data.Item.PostedReference, out var itemNumber))
                         {
                             itemNumber = 1;
-                            while (items.ContainsKey(itemNumber))
+                            while (items.Contains(itemNumber))
                             {
                                 ++itemNumber;
                             }
 
-                            items[itemNumber] = purchaseOrderItem;
-                            purchaseOrderItem.PostedReference = itemNumber.ToString();
+                            items.Add(itemNumber);
+                            data.Item.PostedReference = itemNumber.ToString();
                         }
                         var ci = new PurchaseOrderTimberlineLine
                         {
                             CommitmentID = purchaseOrder.PONumber,
                             ItemNumber = itemNumber,
-                            Description = purchaseOrderItem.Description,
+                            Description = data.POItem.Description,
                             // RetainagePercent = ,
-                            DeliveryDate = purchaseOrderItem.ReceivedDate,
+                            DeliveryDate = data.POItem.ReceivedDate,
                             //ScopeOfWork
-                            Job = purchaseOrderItem.Job.JobNumber,
-                            //Extra = purchaseOrderItem.Job
-                            CostCode = purchaseOrderItem.CostCentre.Code,
-                            //Category = purchaseOrderItem.cat
-                            TaxGroup = purchaseOrderItem.TaxCode.Code,
-                            Units = Math.Round(purchaseOrderItem.Qty, 4),
-                            UnitCost = Math.Round(purchaseOrderItem.Cost, 4),
-                            UnitDescription = purchaseOrderItem.Dimensions.UnitSize,
-                            Amount = Math.Round(purchaseOrderItem.IncTax, 4),
+                            //Extra = poItem.Job
+                            //Category = poItem.cat
+                            TaxGroup = data.POItem.TaxCode.Code,
+                            Units = Math.Round(data.Qty, 4),
+                            UnitCost = Math.Round(data.POCost, 4),
+                            UnitDescription = data.PODimensions.UnitSize,
+                            Amount = Math.Round(data.POItem.IncTax * data.Qty / data.POQty, 4),
                             // BoughtOut
                         };
 
-                        if(!ProcessLine(model, purchaseOrderItem, ci))
+                        var alloc = data.Item as PurchaseOrderItemAllocation;
+                        if(alloc is not null)
+                        {
+                            ci.Job = alloc.Job.JobNumber;
+                        }
+                        else
+                        {
+                            ci.Job = data.POItem.Job.JobNumber;
+                        }
+                        if (!ci.Job.IsNullOrWhiteSpace())
+                        {
+                            ci.CostCode = data.POItem.CostCentre.Code;
+                        }
+
+                        if(!ProcessLine(model, data.POItem, alloc, ci))
                         {
                             success = false;
                             break;
                         }
                         c.Lines.Add(ci);
                     }
+
                     if (success)
                     {
-                        foreach(var item in POItems)
+                        foreach(var data in lineData)
                         {
-                            cs.AddFragment(item);
+                            cs.AddFragment(data.Item);
                         }
                         cs.AddSuccess(purchaseOrder, c);
                     }

+ 75 - 106
prs.shared/Posters/Timberline/StockMovementTimberlinePoster.cs

@@ -320,131 +320,100 @@ public class StockMovementTimberlinePoster : ITimberlinePoster<StockMovement, St
 
         foreach (var transaction in full)
         {
-            var movements = new List<StockMovement>();
-            foreach(var movement in transaction)
-            {
-                if (movement.Type == StockMovementType.Receive)
-                {
-                    // Ignore these ones.
-                    result.AddSuccess(movement, null);
-                }
-                else if(movement.Type == StockMovementType.Issue)
-                {
-                    if(movement.Job.ID == Guid.Empty)
-                    {
-                        movements.Add(movement);
-                    }
-                    else
-                    {
-                        result.AddSuccess(movement, null);
-                    }
-                }
-                else
-                {
-                    // So we only care about transfers and stocktakes
-                    movements.Add(movement);
-                }
-            }
+            var mvts = transaction.ToArray();
 
-            if(movements.Count == 1)
+            // I think we will fail all the movements if any one movement in the transaction failed. All the successful ones,
+            // rather than saving them with AddSuccess immediately, we will put here first, and only succeed them if every movement succeeded.
+            var successful = new List<(StockMovement mvt, IStockMovementTimberlineLine? line)>();
+            foreach(var mvt in mvts)
             {
-                var mvt = movements[0];
-
-                if(mvt.Type == StockMovementType.Issue)
+                switch (mvt.Type)
                 {
-                    var gl = new StockMovementTimberlineGL { };
-                    gl = ModifyLine(gl, mvt);
-                    gl.DebitAccount = Settings.StockTakeGL;
-                    if (ProcessGLLine(model, mvt, gl))
-                    {
-                        result.AddSuccess(mvt, gl);
-                    }
-                    else
-                    {
-                        result.AddFailed(mvt, "Failed by script.");
-                    }
-                }
-                else if(mvt.Type == StockMovementType.StockTake)
-                {
-                    if(mvt.Job.ID == Guid.Empty)
-                    {
-                        var gl = new StockMovementTimberlineGL { };
-                        gl = ModifyLine(gl, mvt);
-                        gl.DebitAccount = Settings.StockTakeGL;
-                        if (ProcessGLLine(model, mvt, gl))
+                    case StockMovementType.Issue:
+                        if(mvt.Job.ID == Guid.Empty)
                         {
-                            result.AddSuccess(mvt, gl);
+                            // Issue to General Stock
+                            var gl = new StockMovementTimberlineGL { };
+                            gl = ModifyLine(gl, mvt);
+                            gl.DebitAccount = Settings.StockTakeGL;
+                            if (ProcessGLLine(model, mvt, gl))
+                            {
+                                successful.Add((mvt, gl));
+                            }
+                            else
+                            {
+                                result.AddFailed(mvt, "Failed by script.");
+                            }
                         }
                         else
                         {
-                            result.AddFailed(mvt, "Failed by script.");
+                            // Ignore issues to a job.
+                            successful.Add((mvt, null));
                         }
-                    }
-                    else
-                    {
-                        var dc = CreateDirectCost(mvt);
-                        if (ProcessDirectCostLine(model, mvt, dc))
+                        break;
+                    case StockMovementType.Receive:
+                        successful.Add((mvt, null));
+                        break;
+                    case StockMovementType.StockTake:
+                        if(mvt.Job.ID == Guid.Empty)
                         {
-                            result.AddSuccess(mvt, dc);
+                            // StockTake in General Stock
+                            var gl = new StockMovementTimberlineGL { };
+                            gl = ModifyLine(gl, mvt);
+                            gl.DebitAccount = Settings.StockTakeGL;
+                            if (ProcessGLLine(model, mvt, gl))
+                            {
+                                successful.Add((mvt, gl));
+                            }
+                            else
+                            {
+                                result.AddFailed(mvt, "Failed by script.");
+                            }
                         }
                         else
                         {
-                            result.AddFailed(mvt, "Failed by script.");
+                            // StockTake in Job Holding
+                            var dc = CreateDirectCost(mvt);
+                            if (ProcessDirectCostLine(model, mvt, dc))
+                            {
+                                successful.Add((mvt, dc));
+                            }
+                            else
+                            {
+                                result.AddFailed(mvt, "Failed by script.");
+                            }
                         }
-                    }
-                }
-                else
-                {
-                    result.AddSuccess(mvt, null);
+                        break;
+                    case StockMovementType.TransferOut:
+                    case StockMovementType.TransferIn:
+                        if(mvt.Job.ID != Guid.Empty)
+                        {
+                            var directCost = CreateDirectCost(mvt);
+                            if(ProcessDirectCostLine(model, mvt, directCost))
+                            {
+                                successful.Add((mvt, directCost));
+                            }
+                            else
+                            {
+                                result.AddFailed(mvt, "Failed by script.");
+                            }
+                        }
+                        break;
                 }
             }
-            else if(movements.Count == 2)
+
+            if(successful.Count < mvts.Length)
             {
-                var mvtFrom = movements[0];
-                var mvtTo = movements[1];
-                if(mvtFrom.Job.ID == mvtTo.Job.ID)
-                {
-                    // Ignore these ones.
-                    result.AddSuccess(mvtFrom, null);
-                    result.AddSuccess(mvtTo, null);
-                }
-                else if(mvtFrom.Job.ID == Guid.Empty || mvtTo.Job.ID == Guid.Empty)
+                foreach(var (mvt, _) in successful)
                 {
-                    var jobMvt = mvtFrom.Job.ID == Guid.Empty ? mvtTo : mvtFrom;
-
-                    var directCost = CreateDirectCost(jobMvt);
-                    if(ProcessDirectCostLine(model, jobMvt, directCost))
-                    {
-                        result.AddSuccess(mvtFrom, directCost);
-                        result.AddSuccess(mvtTo, directCost);
-                    }
-                    else
-                    {
-                        result.AddFailed(mvtFrom, "Failed by script.");
-                        result.AddFailed(mvtTo, "Failed by script.");
-                    }
+                    result.AddFailed(mvt, "Transaction was unsuccessful.");
                 }
-                else
+            }
+            else
+            {
+                foreach(var (mvt, item) in successful)
                 {
-                    var directCostFrom = CreateDirectCost(mvtFrom);
-                    var directCostTo = CreateDirectCost(mvtTo);
-
-                    if (ProcessDirectCostLine(model, mvtFrom, directCostFrom))
-                    {
-                        result.AddSuccess(mvtFrom, directCostFrom);
-                    }
-                    else
-                    {
-                        result.AddFailed(mvtFrom, "Failed by script.");
-                    }
-                    if (ProcessDirectCostLine(model, mvtTo, directCostTo))
-                    {
-                        result.AddSuccess(mvtTo, directCostTo);
-                    }
-                    else
-                    {
-                        result.AddFailed(mvtTo, "Failed by script.");
-                    }
+                    result.AddSuccess(mvt, item);
                 }
             }
         }

+ 2 - 13
prs.stores/PurchaseOrderItemStore.cs

@@ -187,19 +187,8 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
             Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Unit Size is zero!");
             entity.Dimensions.CopyFrom(productrow.ToObject<Product>().DefaultInstance.Dimensions);
         }
-        
-        var dimensions = entity.Dimensions;
-        var qty = entity.Qty;
-        DimensionUtils.ConvertDimensions(
-            dimensions, 
-            ref qty, 
-            (f, c) => DbFactory.NewProvider(InABox.Core.Logger.Main).Query(f, c)
-        );
-        if (!qty.IsEffectivelyEqual(entity.Qty))
-        {
-            entity.Cost = entity.Cost * entity.Qty / (qty.IsEffectivelyEqual(0.0) ? 1.0 : qty);
-            entity.Qty = qty;
-        }
+
+        entity.ConvertDimensions(DbFactory.NewProvider(Logger.Main).QueryProvider<ProductDimensionUnit>());
         // Actual logic begins here.
         
         var totalAllocations = allocations.Sum(x => x.Quantity);

+ 39 - 2
prs.stores/Utilities/DimensionUtils.cs

@@ -34,8 +34,20 @@ public static class DimensionUtils
                 _dimensionscriptcache.Remove(id);
         }
     }
+    public static void ReloadDimensionScriptCache(Guid[]? ids, IQueryProvider<ProductDimensionUnit> query)
+    {
+        ReloadDimensionScriptCache(ids, (f, c) => query.Query(f, c));
+    }
+
+    public static void ConvertDimensions(
+        IDimensions dimensions,
+        ref double quantity,
+        IQueryProvider<ProductDimensionUnit> reload) => ConvertDimensions(dimensions, ref quantity, (f, c) => reload.Query(f, c));
     
-    public static void ConvertDimensions(IDimensions dimensions, ref double quantity, Func<Filter<ProductDimensionUnit>?,Columns<ProductDimensionUnit>,CoreTable> reload)
+    public static void ConvertDimensions(
+        IDimensions dimensions,
+        ref double quantity,
+        Func<Filter<ProductDimensionUnit>?,Columns<ProductDimensionUnit>,CoreTable> reload)
     {
         if (quantity.IsEffectivelyEqual(0.0))
             return;
@@ -52,9 +64,34 @@ public static class DimensionUtils
         }
     }
 
-    public static double ConvertDimensions(IDimensions dimensions, double quantity, Func<Filter<ProductDimensionUnit>?,Columns<ProductDimensionUnit>,CoreTable> reload)
+    public static double ConvertDimensions(IDimensions dimensions, double quantity, Func<Filter<ProductDimensionUnit>?, Columns<ProductDimensionUnit>, CoreTable> reload)
     {
         ConvertDimensions(dimensions, ref quantity, reload);
         return quantity;
     }
+    public static double ConvertDimensions(IDimensions dimensions, double quantity, IQueryProvider<ProductDimensionUnit> query)
+    {
+        ConvertDimensions(dimensions, ref quantity, query);
+        return quantity;
+    }
+
+    /// <summary>
+    /// Convert the dimensions on a <see cref="PurchaseOrderItem"/>, and adjust
+    /// its <see cref="PurchaseOrderItem.Cost"/> and <see
+    /// cref="PurchaseOrderItem.Qty"/> accordingly.
+    /// </summary>
+    public static void ConvertDimensions(this PurchaseOrderItem entity, IQueryProvider<ProductDimensionUnit> query)
+    {
+        var dimensions = entity.Dimensions;
+        var qty = entity.Qty;
+        ConvertDimensions(
+            dimensions, 
+            ref qty,
+            query);
+        if (!qty.IsEffectivelyEqual(entity.Qty))
+        {
+            entity.Cost = entity.Cost * entity.Qty / (qty.IsEffectivelyEqual(0.0) ? 1.0 : qty);
+            entity.Qty = qty;
+        }
+    }
 }