PurchaseOrderTimberlinePoster.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. using Comal.Classes;
  2. using CsvHelper.Configuration.Attributes;
  3. using CsvHelper;
  4. using InABox.Core.Postable;
  5. using InABox.Core;
  6. using InABox.Poster.Timberline;
  7. using InABox.Scripting;
  8. using System.Collections.Generic;
  9. using System.Globalization;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. using Microsoft.Win32;
  15. using CsvHelper.TypeConversion;
  16. using CsvHelper.Configuration;
  17. using System.Reflection;
  18. using System.Windows;
  19. namespace PRS.Shared
  20. {
  21. public enum PurchaseOrderTimberlineCommitmentType
  22. {
  23. Subcontract = 1,
  24. PO = 2
  25. }
  26. public class POTimberlineCommitmentTypeConverter : DefaultTypeConverter
  27. {
  28. public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
  29. {
  30. if(Enum.TryParse<PurchaseOrderTimberlineCommitmentType>(text, out var type))
  31. {
  32. return type;
  33. }
  34. return base.ConvertFromString(text, row, memberMapData);
  35. }
  36. public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
  37. {
  38. if(value is PurchaseOrderTimberlineCommitmentType type)
  39. {
  40. return ((int)type).ToString();
  41. }
  42. return "";
  43. }
  44. }
  45. public class PurchaseOrderTimberlineHeader
  46. {
  47. [Ignore]
  48. public List<PurchaseOrderTimberlineLine> Lines { get; set; } = new();
  49. [Index(0)]
  50. public string RecordID { get; set; } = "C";
  51. [Index(1)]
  52. [TypeConverter(typeof(TimberlinePosterStringConverter), 12)]
  53. public string CommitmentID { get; set; }
  54. [Index(2)]
  55. [TypeConverter(typeof(POTimberlineCommitmentTypeConverter))]
  56. public PurchaseOrderTimberlineCommitmentType CommitmentType { get; set; }
  57. [Index(3)]
  58. [TypeConverter(typeof(TimberlinePosterStringConverter), 30)]
  59. public string Description { get; set; }
  60. [Index(4)]
  61. [TypeConverter(typeof(TimberlinePosterStringConverter), 10)]
  62. public string VendorID { get; set; }
  63. [Index(5)]
  64. [TypeConverter(typeof(TimberlinePosterDateConverter))]
  65. public DateTime Date { get; set; }
  66. [Index(6)]
  67. public double RetainagePercent { get; set; }
  68. [Index(7)]
  69. public bool CommittedToJC { get; set; }
  70. [Index(8)]
  71. public bool Closed { get; set; }
  72. [Index(9)]
  73. public bool Printed { get; set; }
  74. /// <summary>
  75. /// Dictionary of extra fields to write; the key is the 0-based index of the column in which we are writing.
  76. /// If the index is that of any of the explicit fields of this class, nothing happens.
  77. /// </summary>
  78. [Ignore]
  79. public Dictionary<int, string> AdditionalFields { get; set; } = new();
  80. }
  81. public class PurchaseOrderTimberlineLine
  82. {
  83. [Index(0)]
  84. public string RecordID { get; set; } = "CI";
  85. [Index(1)]
  86. [TypeConverter(typeof(TimberlinePosterStringConverter), 12)]
  87. public string CommitmentID { get; set; }
  88. [Index(2)]
  89. public int ItemNumber { get; set; }
  90. [Index(3)]
  91. [TypeConverter(typeof(TimberlinePosterStringConverter), 30)]
  92. public string Description { get; set; }
  93. [Index(4)]
  94. public double RetainagePercent { get; set; }
  95. [Index(5)]
  96. [TypeConverter(typeof(TimberlinePosterDateConverter))]
  97. public DateTime DeliveryDate { get; set; }
  98. [Index(6)]
  99. [TypeConverter(typeof(TimberlinePosterStringConverter), 1000)]
  100. public string ScopeOfWork { get; set; }
  101. [Index(7)]
  102. [TypeConverter(typeof(TimberlinePosterStringConverter), 10)]
  103. public string Job { get; set; }
  104. [Index(8)]
  105. [TypeConverter(typeof(TimberlinePosterStringConverter), 10)]
  106. public string Extra { get; set; }
  107. [Index(9)]
  108. [TypeConverter(typeof(TimberlinePosterStringConverter), 12)]
  109. public string CostCode { get; set; }
  110. [Index(10)]
  111. [TypeConverter(typeof(TimberlinePosterStringConverter), 3)]
  112. public string Category { get; set; }
  113. [Index(11)]
  114. [TypeConverter(typeof(TimberlinePosterStringConverter), 6)]
  115. public string TaxGroup { get; set; }
  116. [Index(12)]
  117. public string Tax { get; set; }
  118. [Index(13)]
  119. public double Units { get; set; }
  120. [Index(14)]
  121. public double UnitCost { get; set; }
  122. [Index(15)]
  123. [TypeConverter(typeof(TimberlinePosterStringConverter), 6)]
  124. public string UnitDescription { get; set; }
  125. [Index(16)]
  126. public double Amount { get; set; }
  127. [Index(17)]
  128. public bool BoughtOut { get; set; }
  129. /// <summary>
  130. /// Dictionary of extra fields to write; the key is the 0-based index of the column in which we are writing.
  131. /// If the index is that of any of the explicit fields of this class, nothing happens.
  132. /// </summary>
  133. [Ignore]
  134. public Dictionary<int, string> AdditionalFields { get; set; } = new();
  135. }
  136. public class PurchaseOrderTimberlineSettings : TimberlinePosterSettings<PurchaseOrder>
  137. {
  138. protected override string DefaultScript()
  139. {
  140. return @"
  141. using PRS.Shared;
  142. using InABox.Core;
  143. using System.Collections.Generic;
  144. public class Module
  145. {
  146. public void BeforePost(IDataModel<PurchaseOrder> model)
  147. {
  148. // Perform pre-processing
  149. }
  150. public void ProcessHeader(IDataModel<PurchaseOrder> model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header)
  151. {
  152. // Do extra processing for a purchase order; return false to fail this purchase order
  153. return true;
  154. }
  155. public void ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
  156. {
  157. // Do extra processing for a purchase order line; return false to fail this purchase order
  158. return true;
  159. }
  160. public void AfterPost(IDataModel<PurchaseOrder> model)
  161. {
  162. // Perform post-processing
  163. }
  164. }";
  165. }
  166. }
  167. public class PurchaseOrderTimberlineResult : TimberlinePostResult<PurchaseOrderTimberlineHeader, PurchaseOrder>
  168. {
  169. }
  170. public class PurchaseOrderTimberlinePoster : ITimberlinePoster<PurchaseOrder, PurchaseOrderTimberlineSettings>
  171. {
  172. public ScriptDocument? Script { get; set; }
  173. public PurchaseOrderTimberlineSettings Settings { get; set; }
  174. public bool BeforePost(IDataModel<PurchaseOrder> model)
  175. {
  176. model.SetIsDefault<Document>(false, alias: "CompanyLogo");
  177. model.SetIsDefault<CoreTable>(false, alias: "CompanyInformation");
  178. model.SetIsDefault<Employee>(false);
  179. model.SetIsDefault<PurchaseOrderItem>(true, alias: "PurchaseOrder_PurchaseOrderItem");
  180. model.SetColumns(Columns.None<PurchaseOrder>().Add(x => x.ID)
  181. .Add(x => x.PONumber)
  182. .Add(x => x.Description)
  183. .Add(x => x.SupplierLink.Code)
  184. .Add(x => x.IssuedDate)
  185. .Add(x => x.ClosedDate));
  186. model.SetColumns(Columns.None<PurchaseOrderItem>().Add(x => x.ID)
  187. .Add(x => x.PurchaseOrderLink.ID)
  188. .Add(x => x.PostedReference)
  189. .Add(x => x.Description)
  190. .Add(x => x.ReceivedDate)
  191. .Add(x => x.CostCentre.Code)
  192. .Add(x => x.TaxCode.Code)
  193. .Add(x => x.Qty)
  194. .Add(x => x.Cost)
  195. .Add(x => x.Dimensions.UnitSize)
  196. .Add(x => x.IncTax)
  197. .Add(x => x.Job.JobNumber),
  198. alias: "PurchaseOrder_PurchaseOrderItem");
  199. Script?.Execute(methodname: "BeforePost", parameters: new object[] { model });
  200. return true;
  201. }
  202. private bool ProcessHeader(IDataModel<PurchaseOrder> model, PurchaseOrder purchaseOrder, PurchaseOrderTimberlineHeader header)
  203. {
  204. return Script?.Execute(methodname: "ProcessHeader", parameters: new object[] { model, purchaseOrder, header }) != false;
  205. }
  206. private bool ProcessLine(IDataModel<PurchaseOrder> model, PurchaseOrderItem purchaseOrderItem, PurchaseOrderTimberlineLine line)
  207. {
  208. return Script?.Execute(methodname: "ProcessLine", parameters: new object[] { model, purchaseOrderItem, line }) != false;
  209. }
  210. private PurchaseOrderTimberlineResult DoProcess(IDataModel<PurchaseOrder> model)
  211. {
  212. var cs = new PurchaseOrderTimberlineResult();
  213. var lines = model.GetTable<PurchaseOrderItem>("PurchaseOrder_PurchaseOrderItem").ToObjects<PurchaseOrderItem>()
  214. .GroupBy(x => x.PurchaseOrderLink.ID).ToDictionary(x => x.Key, x => x.ToList());
  215. foreach (var purchaseOrder in model.GetTable<PurchaseOrder>().ToObjects<PurchaseOrder>())
  216. {
  217. var c = new PurchaseOrderTimberlineHeader
  218. {
  219. CommitmentID = purchaseOrder.PONumber,
  220. CommitmentType = PurchaseOrderTimberlineCommitmentType.PO,
  221. Description = purchaseOrder.Description,
  222. VendorID = purchaseOrder.SupplierLink.Code,
  223. Date = purchaseOrder.IssuedDate,
  224. // RetainagePercent
  225. // Committed to JC
  226. Closed = purchaseOrder.ClosedDate != DateTime.MinValue,
  227. // Printed
  228. };
  229. if(!ProcessHeader(model, purchaseOrder, c))
  230. {
  231. cs.AddFailed(purchaseOrder, "Failed by script.");
  232. }
  233. else
  234. {
  235. // Dictionary from line number to POItem.
  236. var items = new Dictionary<int, PurchaseOrderItem>();
  237. var POItems = lines.GetValueOrDefault(purchaseOrder.ID)?.ToList() ?? new List<PurchaseOrderItem>();
  238. foreach (var purchaseOrderItem in POItems)
  239. {
  240. if (int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
  241. {
  242. if (items.TryGetValue(itemNumber, out var oldItem))
  243. {
  244. // Theoretically shouldn't happen, but just in case.
  245. 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.");
  246. Logger.Send(LogType.Error, "", $"Purchase Order Post: Multiple POItems with the same Line Number; changing line number of POItem {purchaseOrderItem.ID}");
  247. purchaseOrderItem.PostedReference = "";
  248. }
  249. else
  250. {
  251. items[itemNumber] = purchaseOrderItem;
  252. }
  253. }
  254. }
  255. var success = true;
  256. foreach (var purchaseOrderItem in POItems)
  257. {
  258. if (!int.TryParse(purchaseOrderItem.PostedReference, out var itemNumber))
  259. {
  260. itemNumber = 1;
  261. while (items.ContainsKey(itemNumber))
  262. {
  263. ++itemNumber;
  264. }
  265. items[itemNumber] = purchaseOrderItem;
  266. purchaseOrderItem.PostedReference = itemNumber.ToString();
  267. }
  268. var ci = new PurchaseOrderTimberlineLine
  269. {
  270. CommitmentID = purchaseOrder.PONumber,
  271. ItemNumber = itemNumber,
  272. Description = purchaseOrderItem.Description,
  273. // RetainagePercent = ,
  274. DeliveryDate = purchaseOrderItem.ReceivedDate,
  275. //ScopeOfWork
  276. Job = purchaseOrderItem.Job.JobNumber,
  277. //Extra = purchaseOrderItem.Job
  278. CostCode = purchaseOrderItem.CostCentre.Code,
  279. //Category = purchaseOrderItem.cat
  280. TaxGroup = purchaseOrderItem.TaxCode.Code,
  281. Units = Math.Round(purchaseOrderItem.Qty, 4),
  282. UnitCost = Math.Round(purchaseOrderItem.Cost, 4),
  283. UnitDescription = purchaseOrderItem.Dimensions.UnitSize,
  284. Amount = Math.Round(purchaseOrderItem.IncTax, 4),
  285. // BoughtOut
  286. };
  287. if(!ProcessLine(model, purchaseOrderItem, ci))
  288. {
  289. success = false;
  290. break;
  291. }
  292. c.Lines.Add(ci);
  293. }
  294. if (success)
  295. {
  296. foreach(var item in POItems)
  297. {
  298. cs.AddFragment(item);
  299. }
  300. cs.AddSuccess(purchaseOrder, c);
  301. }
  302. else
  303. {
  304. cs.AddFailed(purchaseOrder, "Failed by script.");
  305. }
  306. }
  307. }
  308. return cs;
  309. }
  310. public IPostResult<PurchaseOrder> Process(IDataModel<PurchaseOrder> model)
  311. {
  312. var POs = DoProcess(model);
  313. var dlg = new SaveFileDialog()
  314. {
  315. Filter = "CSV Files (*.csv)|*.csv"
  316. };
  317. if (dlg.ShowDialog() == true)
  318. {
  319. using (var writer = new StreamWriter(dlg.FileName))
  320. {
  321. using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
  322. foreach (var header in POs.Exports)
  323. {
  324. // Write the record.
  325. csv.WriteRecord(header);
  326. // Current 0-based index that the writer is at.
  327. int i = csv.Index;
  328. foreach (var (index, field) in header.AdditionalFields.OrderBy(x => x.Key))
  329. {
  330. while (i < index)
  331. {
  332. csv.WriteField("");
  333. ++i;
  334. }
  335. csv.WriteField(field);
  336. ++i;
  337. }
  338. csv.NextRecord();
  339. foreach (var poi in header.Lines)
  340. {
  341. csv.WriteRecord(poi);
  342. // Current 0-based index that the writer is at.
  343. i = csv.Index;
  344. foreach (var (index, field) in poi.AdditionalFields.OrderBy(x => x.Key))
  345. {
  346. while (i < index)
  347. {
  348. csv.WriteField("");
  349. ++i;
  350. }
  351. csv.WriteField(field);
  352. ++i;
  353. }
  354. csv.NextRecord();
  355. }
  356. }
  357. }
  358. while (true)
  359. {
  360. var logDlg = new OpenFileDialog
  361. {
  362. InitialDirectory = Path.GetDirectoryName(dlg.FileName),
  363. FileName = "JCREJECT.JCC",
  364. Filter = "Rejected Item Files (*.jcc) | *.jcc;*.JCC | All Files (*.*) | *.*",
  365. Title = "Please select JCREJECT.JCC"
  366. };
  367. if (logDlg.ShowDialog() == true)
  368. {
  369. var rejectedHeaders = new List<PurchaseOrderTimberlineHeader?>();
  370. var rejectedLines = new List<PurchaseOrderTimberlineLine?>();
  371. using (var reader = new StreamReader(logDlg.FileName))
  372. {
  373. using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture)
  374. {
  375. HasHeaderRecord = false
  376. });
  377. while (csv.Read())
  378. {
  379. var id = csv.GetField(0);
  380. if (id == "C")
  381. {
  382. var header = csv.GetRecord<PurchaseOrderTimberlineHeader>();
  383. if (header is not null)
  384. {
  385. var entry = POs.Items.FirstOrDefault(x => x.Item2?.CommitmentID.Equals(header.CommitmentID) == true);
  386. if (entry is not null)
  387. {
  388. (entry.Item1 as IPostable).FailPost("");
  389. }
  390. }
  391. else
  392. {
  393. Logger.Send(LogType.Error, "", "PO Timberline export: Unable to parse header from CSV line in rejection file.");
  394. MessageBox.Show("Invalid line in file; skipping.");
  395. }
  396. }
  397. else if (id == "CI")
  398. {
  399. var line = csv.GetRecord<PurchaseOrderTimberlineLine>();
  400. if (line is not null)
  401. {
  402. var entry = POs.Items.FirstOrDefault(x => x.Item2?.CommitmentID.Equals(line.CommitmentID) == true);
  403. if (entry is not null)
  404. {
  405. (entry.Item1 as IPostable).FailPost("");
  406. }
  407. }
  408. else
  409. {
  410. Logger.Send(LogType.Error, "", "PO Timberline export: Unable to parse line from CSV line in rejection file.");
  411. MessageBox.Show("Invalid line in file; skipping.");
  412. }
  413. }
  414. }
  415. }
  416. return POs;
  417. }
  418. else
  419. {
  420. if (MessageBox.Show("Do you wish to cancel the export?", "Cancel Export?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
  421. {
  422. throw new PostCancelledException();
  423. }
  424. else if (MessageBox.Show("Did everything post successfully?", "Successful?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
  425. {
  426. return POs;
  427. }
  428. }
  429. }
  430. }
  431. else
  432. {
  433. throw new PostCancelledException();
  434. }
  435. }
  436. public void AfterPost(IDataModel<PurchaseOrder> model, IPostResult<PurchaseOrder> result)
  437. {
  438. Script?.Execute(methodname: "AfterPost", parameters: new object[] { model });
  439. }
  440. }
  441. public class PurchaseOrderTimberlinePosterEngine<T> : TimberlinePosterEngine<PurchaseOrder, PurchaseOrderTimberlineSettings>
  442. {
  443. }
  444. }