using Comal.Classes; using CsvHelper.Configuration.Attributes; using CsvHelper.Configuration; using CsvHelper; using InABox.Core.Postable; using InABox.Core; using InABox.Poster.Timberline; using InABox.Scripting; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using Microsoft.Win32; using CsvHelper.TypeConversion; using NPOI.SS.Formula.Functions; using Columns = InABox.Core.Columns; namespace PRS.Shared; public interface IStockMovementTimberlineLine { DateTime TransactionDate { get; set; } DateTime AccountingDate { get; set; } string Description { get; set; } double Amount { get; set; } string DebitAccount { get; set; } string CreditAccount { get; set; } string Reference1 { get; set; } string Reference2 { get; set; } } public enum StockMovementTimberlineTransactionType { APCost = 1, JCCost = 2, PRCost = 3, EQCost = 4, IVCost = 5 } public class StockMovementTimberlineTransactionTypeConverter : DefaultTypeConverter { public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData) { if (Enum.TryParse(text, out var type)) { return type; } return base.ConvertFromString(text, row, memberMapData); } public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData) { if (value is StockMovementTimberlineTransactionType type) { return ((int)type).ToString(); } return ""; } } public class StockMovementTimberlineDirectCost : IStockMovementTimberlineLine { [Ignore] public StockMovementBatchType BatchType { get; set; } [Index(0)] public string RecordID { get; set; } = "DC"; [Index(1)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Job { get; set; } [Index(2)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Extra { get; set; } [Index(3)] [TypeConverter(typeof(TimberlinePosterStringConverter), 12)] public string CostCode { get; set; } [Index(4)] [TypeConverter(typeof(TimberlinePosterStringConverter), 3)] public string Category { get; set; } [Index(5)] [TypeConverter(typeof(StockMovementTimberlineTransactionTypeConverter))] public StockMovementTimberlineTransactionType TransactionType { get; set; } [Index(6)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime TransactionDate { get; set; } [Index(7)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime AccountingDate { get; set; } [Index(8)] [TypeConverter(typeof(TimberlinePosterStringConverter), 30)] public string Description { get; set; } [Index(9)] public double Units { get; set; } [Index(10)] public double UnitCost { get; set; } [Index(11)] public double Amount { get; set; } [Index(12)] [TypeConverter(typeof(TimberlinePosterStringConverter), 25)] public string DebitAccount { get; set; } [Index(13)] [TypeConverter(typeof(TimberlinePosterStringConverter), 25)] public string CreditAccount { get; set; } [Index(14)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Reference1 { get; set; } [Index(15)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Reference2 { get; set; } [Index(16)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string StandardItem { get; set; } } public class StockMovementTimberlineGL : IStockMovementTimberlineLine { [Ignore] public StockMovementBatchType BatchType { get; set; } [Index(0)] public string RecordID { get; set; } = "GL"; [Index(1)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime TransactionDate { get; set; } [Index(2)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime AccountingDate { get; set; } [Index(3)] [TypeConverter(typeof(TimberlinePosterStringConverter), 30)] public string Description { get; set; } [Index(4)] public double Amount { get; set; } [Index(5)] [TypeConverter(typeof(TimberlinePosterStringConverter), 25)] public string DebitAccount { get; set; } [Index(6)] [TypeConverter(typeof(TimberlinePosterStringConverter), 25)] public string CreditAccount { get; set; } [Index(7)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Reference1 { get; set; } [Index(8)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Reference2 { get; set; } } public class StockMovementTimberlineSettings : TimberlinePosterSettings { [TextBoxEditor] public string StockTakeGL { get; set; } protected override string DefaultScript() { return @" using PRS.Shared; using InABox.Core; using System.Collections.Generic; public class Module { public void BeforePost(IDataModel model) { // Perform pre-processing } public bool ProcessDirectCostLine(IDataModel model, StockMovement stockMovement, StockMovementTimberlineDirectCost line) { // Do extra processing for a direct cost line; return false to fail this movement return true; } public bool ProcessGLLine(IDataModel model, StockMovement stockMovement, StockMovementTimberlineGL line) { // Do extra processing for a GL line; return false to fail this movement return true; } public void AfterPost(IDataModel model) { // Perform post-processing } }"; } } public class StockMovementTimberlineResult : TimberlinePostResult { } public class StockMovementTimberlinePoster : ITimberlinePoster { public ScriptDocument? Script { get; set; } public StockMovementTimberlineSettings Settings { get; set; } public bool BeforePost(IDataModel model) { model.SetIsDefault(false, alias: "CompanyLogo"); model.SetIsDefault(false, alias: "CompanyInformation"); model.SetIsDefault(false); model.SetColumns(Columns.None().Add(x => x.ID).Add(x => x.Transaction)); model.AddChildTable(x => x.Transaction, x => x.Transaction, parentalias: "StockMovement", childalias: "FullTransactions", isdefault: true, columns: Columns.None().Add(x => x.ID) .Add(x => x.Transaction) .Add(x => x.Job.ID) .Add(x => x.Product.ID) .Add(x => x.Type) .Add(x => x.Units) .Add(x => x.Cost) .Add(x => x.Value) .Add(x => x.Date) .Add(x => x.Batch.ID) ); model.AddLookupTable(x => x.Product.ID, x => x.ID, sourcealias: "FullTransactions", isdefault: true, columns: Columns.None().Add(x => x.ID) .Add(x => x.Name) .Add(x => x.CostCentre.Code) .Add(x => x.PurchaseGL.Code)); model.AddLookupTable(x => x.Job.ID, x => x.ID, sourcealias: "FullTransactions", isdefault: true, columns: Columns.None().Add(x => x.ID) .Add(x => x.JobNumber)); Script?.Execute(methodname: "BeforePost", parameters: new object[] { model }); return true; } private bool ProcessDirectCostLine(IDataModel model, StockMovement stockMovement, StockMovementTimberlineDirectCost line) { return Script?.Execute(methodname: "ProcessDirectCostLine", parameters: new object[] { model, stockMovement, line }) != false; } private bool ProcessGLLine(IDataModel model, StockMovement stockMovement, StockMovementTimberlineGL line) { return Script?.Execute(methodname: "ProcessGLLine", parameters: new object[] { model, stockMovement, line }) != false; } private StockMovementTimberlineResult DoProcess(IDataModel model) { var result = new StockMovementTimberlineResult(); var firstMovements = model.GetTable(); var full = model.GetTable("FullTransactions") .ToObjects().GroupBy(x => x.Transaction); var products = model.GetTable().ToObjects() .ToDictionary(x => x.ID, x => x); var jobs = model.GetTable().ToObjects() .ToDictionary(x => x.ID, x => x); StockMovementTimberlineDirectCost CreateDirectCost(StockMovement movement) { var job = jobs[movement.Job.ID]; var product = products[movement.Product.ID]; var directCost = new StockMovementTimberlineDirectCost { Job = job.JobNumber, Extra = "", CostCode = product.CostCentre.Code, Category = "", Units = Math.Round(movement.Units, 4), UnitCost = Math.Round(movement.Cost, 4), TransactionType = StockMovementTimberlineTransactionType.IVCost }; return ModifyLine(directCost, movement); } T ModifyLine(T line, StockMovement movement) where T : IStockMovementTimberlineLine { var product = products[movement.Product.ID]; line.TransactionDate = movement.Date; line.AccountingDate = movement.Date; line.Description = product.Name; line.Amount = Math.Round(movement.Value, 4); line.CreditAccount = product.PurchaseGL.Code; line.DebitAccount = product.SellGL.Code; return line; } foreach (var transaction in full) { var mvts = transaction.ToArray(); // 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) { switch (mvt.Type) { case StockMovementType.Issue: if(mvt.Job.ID == Guid.Empty) { // 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 { // Ignore issues to a job. successful.Add((mvt, null)); } break; case StockMovementType.Receive: successful.Add((mvt, null)); break; case StockMovementType.StockTake: if(mvt.Job.ID == Guid.Empty) { // 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 { // StockTake in Job Holding var dc = CreateDirectCost(mvt); if (ProcessDirectCostLine(model, mvt, dc)) { successful.Add((mvt, dc)); } else { result.AddFailed(mvt, "Failed by script."); } } 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; } } if(successful.Count < mvts.Length) { foreach(var (mvt, _) in successful) { result.AddFailed(mvt, "Transaction was unsuccessful."); } } else { foreach(var (mvt, item) in successful) { result.AddSuccess(mvt, item); } } } return result; } public IPostResult Process(IDataModel model) { var result = DoProcess(model); var dlg = new SaveFileDialog() { Title = "Select Output File", Filter = "CSV Files (*.csv)|*.csv" }; if (dlg.ShowDialog() == true) { using (var writer = new StreamWriter(dlg.FileName)) { using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); foreach (var line in result.Exports.Distinct()) { // Write the record. if(line is StockMovementTimberlineDirectCost dc) { csv.WriteRecord(dc); } else if(line is StockMovementTimberlineGL gl) { csv.WriteRecord(gl); } csv.NextRecord(); } } } else { throw new PostCancelledException(); } return result; } public void AfterPost(IDataModel model, IPostResult result) { Script?.Execute(methodname: "AfterPost", parameters: new object[] { model }); } } public class StockMovementTimberlinePosterEngine : TimberlinePosterEngine { }