using Comal.Classes; using CsvHelper.Configuration.Attributes; using CsvHelper; using InABox.Core.Postable; using InABox.Core; using InABox.Poster.Timberline; using InABox.Scripting; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Win32; using CsvHelper.TypeConversion; using CsvHelper.Configuration; using System.Reflection; using System.Windows; namespace PRS.Shared { public enum PurchaseOrderTimberlineCommitmentType { Subcontract = 1, PO = 2 } public class POTimberlineCommitmentTypeConverter : 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 PurchaseOrderTimberlineCommitmentType type) { return ((int)type).ToString(); } return ""; } } public class PurchaseOrderTimberlineHeader { [Ignore] public List Lines { get; set; } = new(); [Index(0)] public string RecordID { get; set; } = "C"; [Index(1)] [TypeConverter(typeof(TimberlinePosterStringConverter), 12)] public string CommitmentID { get; set; } [Index(2)] [TypeConverter(typeof(POTimberlineCommitmentTypeConverter))] public PurchaseOrderTimberlineCommitmentType CommitmentType { get; set; } [Index(3)] [TypeConverter(typeof(TimberlinePosterStringConverter), 30)] public string Description { get; set; } [Index(4)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string VendorID { get; set; } [Index(5)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime Date { get; set; } [Index(6)] public double RetainagePercent { get; set; } [Index(7)] public bool CommittedToJC { get; set; } [Index(8)] public bool Closed { get; set; } [Index(9)] public bool Printed { get; set; } /// /// Dictionary of extra fields to write; the key is the 0-based index of the column in which we are writing. /// If the index is that of any of the explicit fields of this class, nothing happens. /// [Ignore] public Dictionary AdditionalFields { get; set; } = new(); } public class PurchaseOrderTimberlineLine { [Index(0)] public string RecordID { get; set; } = "CI"; [Index(1)] [TypeConverter(typeof(TimberlinePosterStringConverter), 12)] public string CommitmentID { get; set; } [Index(2)] public int ItemNumber { get; set; } [Index(3)] [TypeConverter(typeof(TimberlinePosterStringConverter), 30)] public string Description { get; set; } [Index(4)] public double RetainagePercent { get; set; } [Index(5)] [TypeConverter(typeof(TimberlinePosterDateConverter))] public DateTime DeliveryDate { get; set; } [Index(6)] [TypeConverter(typeof(TimberlinePosterStringConverter), 1000)] public string ScopeOfWork { get; set; } [Index(7)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Job { get; set; } [Index(8)] [TypeConverter(typeof(TimberlinePosterStringConverter), 10)] public string Extra { get; set; } [Index(9)] [TypeConverter(typeof(TimberlinePosterStringConverter), 12)] public string CostCode { get; set; } [Index(10)] [TypeConverter(typeof(TimberlinePosterStringConverter), 3)] public string Category { get; set; } [Index(11)] [TypeConverter(typeof(TimberlinePosterStringConverter), 6)] public string TaxGroup { get; set; } [Index(12)] public string Tax { get; set; } [Index(13)] public double Units { get; set; } [Index(14)] public double UnitCost { get; set; } [Index(15)] [TypeConverter(typeof(TimberlinePosterStringConverter), 6)] public string UnitDescription { get; set; } [Index(16)] public double Amount { get; set; } [Index(17)] public bool BoughtOut { get; set; } /// /// Dictionary of extra fields to write; the key is the 0-based index of the column in which we are writing. /// If the index is that of any of the explicit fields of this class, nothing happens. /// [Ignore] public Dictionary AdditionalFields { get; set; } = new(); } public class PurchaseOrderTimberlineSettings : TimberlinePosterSettings { 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 void ProcessHeader(IDataModel model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header) { // Do extra processing for a purchase order; return false to fail this purchase order return true; } public void ProcessLine(IDataModel model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line) { // Do extra processing for a purchase order line; return false to fail this purchase order return true; } public void AfterPost(IDataModel model) { // Perform post-processing } }"; } } public class PurchaseOrderTimberlineResult : TimberlinePostResult { } public class PurchaseOrderTimberlinePoster : ITimberlinePoster { public ScriptDocument? Script { get; set; } public PurchaseOrderTimberlineSettings Settings { get; set; } public bool BeforePost(IDataModel model) { model.SetIsDefault(false, alias: "CompanyLogo"); model.SetIsDefault(false, alias: "CompanyInformation"); model.SetIsDefault(false); model.SetIsDefault(true, alias: "PurchaseOrder_PurchaseOrderItem"); model.SetColumns(new Columns(x => x.ID) .Add(x => x.PONumber) .Add(x => x.Description) .Add(x => x.SupplierLink.Code) .Add(x => x.IssuedDate) .Add(x => x.ClosedDate)); model.SetColumns(new Columns(x => x.ID) .Add(x => x.PurchaseOrderLink.ID) .Add(x => x.PostedReference) .Add(x => x.Description) .Add(x => x.ReceivedDate) .Add(x => x.Job.JobNumber) .Add(x => x.CostCentre.Code) .Add(x => x.TaxCode.Code) .Add(x => x.Qty) .Add(x => x.Cost) .Add(x => x.Dimensions.UnitSize) .Add(x => x.IncTax), alias: "PurchaseOrder_PurchaseOrderItem"); Script?.Execute(methodname: "BeforePost", parameters: new object[] { model }); return true; } private bool ProcessHeader(IDataModel model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header) { return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, purchaseOrder, header }) != false; } private bool ProcessLine(IDataModel model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line) { return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, purchaseOrderItem, line }) != false; } private PurchaseOrderTimberlineResult DoProcess(IDataModel model) { var cs = new PurchaseOrderTimberlineResult(); var lines = model.GetTable("PurchaseOrder_PurchaseOrderItem").ToObjects() .GroupBy(x => x.PurchaseOrderLink.ID).ToDictionary(x => x.Key, x => x.ToList()); foreach (var purchaseOrder in model.GetTable().ToObjects()) { var c = new PurchaseOrderTimberlineHeader { CommitmentID = purchaseOrder.PONumber, CommitmentType = PurchaseOrderTimberlineCommitmentType.PO, Description = purchaseOrder.Description, VendorID = purchaseOrder.SupplierLink.Code, Date = purchaseOrder.IssuedDate, // RetainagePercent // Committed to JC Closed = purchaseOrder.ClosedDate != DateTime.MinValue, // Printed }; if(!ProcessHeader(model, purchaseOrder, c)) { cs.AddFailed(purchaseOrder, "Failed by script."); } else { // Dictionary from line number to POItem. var items = new Dictionary(); var POItems = lines.GetValueOrDefault(purchaseOrder.ID)?.ToList() ?? new List(); foreach (var purchaseOrderItem in POItems) { if (int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber)) { 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; } } } 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); } if (success) { foreach(var item in POItems) { cs.AddFragment(item); } cs.AddSuccess(purchaseOrder, c); } else { cs.AddFailed(purchaseOrder, "Failed by script."); } } } return cs; } public IPostResult Process(IDataModel model) { var POs = DoProcess(model); var dlg = new SaveFileDialog() { 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 header in POs.Exports) { // Write the record. csv.WriteRecord(header); // Current 0-based index that the writer is at. int i = csv.Index; foreach (var (index, field) in header.AdditionalFields.OrderBy(x => x.Key)) { while (i < index) { csv.WriteField(""); ++i; } csv.WriteField(field); ++i; } csv.NextRecord(); foreach (var poi in header.Lines) { csv.WriteRecord(poi); // Current 0-based index that the writer is at. i = csv.Index; foreach (var (index, field) in poi.AdditionalFields.OrderBy(x => x.Key)) { while (i < index) { csv.WriteField(""); ++i; } csv.WriteField(field); ++i; } csv.NextRecord(); } } } while (true) { var logDlg = new OpenFileDialog { InitialDirectory = Path.GetDirectoryName(dlg.FileName), FileName = "JCREJECT.JCC", Filter = "Rejected Item Files (*.jcc) | *.jcc;*.JCC | All Files (*.*) | *.*", Title = "Please select JCREJECT.JCC" }; if (logDlg.ShowDialog() == true) { var rejectedHeaders = new List(); var rejectedLines = new List(); using (var reader = new StreamReader(logDlg.FileName)) { using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = false }); while (csv.Read()) { var id = csv.GetField(0); if (id == "C") { var header = csv.GetRecord(); 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") { var line = csv.GetRecord(); 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 POs; } else { if (MessageBox.Show("Do you wish to cancel the export?", "Cancel Export?", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { throw new PostCancelledException(); } else if (MessageBox.Show("Did everything post successfully?", "Successful?", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { return POs; } } } } else { throw new PostCancelledException(); } } public void AfterPost(IDataModel model, IPostResult result) { Script?.Execute(methodname: "AfterPost", parameters: new object[] { model }); } } public class PurchaseOrderTimberlinePosterEngine : TimberlinePosterEngine { } }