PurchaseOrderItemAllocation.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using InABox.Core;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. namespace Comal.Classes
  7. {
  8. /// <summary>
  9. /// The <see cref="PurchaseOrderItemAllocation"/> is the way that we are allowing a single <see cref="PurchaseOrderItem"/> to, when received,
  10. /// have its stock distributed to multiple sources. The current idea is this:
  11. ///
  12. /// <list type="bullet">
  13. /// <item>
  14. /// The <see cref="PurchaseOrderItem"/> itself has a <see cref="PurchaseOrderItem.Job"/> and a <see cref="PurchaseOrderItem.Qty"/>.
  15. /// In most cases, this will be the target for received stock, and
  16. /// in this simple case, there are no POIAs. If the <see cref="PurchaseOrderItem.Job"/> is blank, it goes to general stock;
  17. /// otherwise, it goes to a specific allocation.
  18. /// </item>
  19. /// <item>
  20. /// If there are allocations, this cuts into the <see cref="PurchaseOrderItem.Qty"/>, such that after any allocations have been filled,
  21. /// the remainder of the stock goes to the <see cref="PurchaseOrderItem.Job"/>.
  22. /// If the allocations total more than <see cref="PurchaseOrderItem.Qty"/>, then a negative stock movement
  23. /// is registered against <see cref="PurchaseOrderItem.Job"/>.
  24. /// </item>
  25. /// <item>
  26. /// In the case of Dimension conversions (see <see cref="DimensionUnit.Conversion"/>), the quantity on the allocation is after the conversion has
  27. /// happened.<br/>
  28. /// This is important, because in this case, <b>there will be a discrepancy</b> between <see cref="PurchaseOrderItem.Qty"/> and the total
  29. /// of <see cref="Quantity"/>.
  30. /// <br/>
  31. /// As an example, suppose we are purchasing 2 boxes of 1000 screws. Then, the allocations are by numbers of screws, so that 800 might go to
  32. /// Job A, 600 to Job B and 600 to general stock. So the allocation quantities are the actual numbers that will be on the stock movements.
  33. /// </item>
  34. /// </list>
  35. /// </summary>
  36. [Caption("Allocation")]
  37. public class PurchaseOrderItemAllocation : Entity, IRemotable, IPersistent, ILicense<ProjectManagementLicense>
  38. , IOneToMany<JobRequisitionItem>, IOneToMany<Job>, IOneToMany<PurchaseOrderItem>, IPostableFragment<PurchaseOrder>
  39. {
  40. [EntityRelationship(DeleteAction.Cascade)]
  41. public PurchaseOrderItemLink Item { get; set; }
  42. [EntityRelationship(DeleteAction.Cascade)]
  43. [EditorSequence(1)]
  44. public JobLink Job { get; set; }
  45. private class JobRequisitionItemLookup : LookupDefinitionGenerator<JobRequisitionItem, PurchaseOrderItemAllocation>
  46. {
  47. public override Columns<PurchaseOrderItemAllocation> DefineFilterColumns()
  48. {
  49. return base.DefineFilterColumns().Add(x => x.Job.ID).Add(x => x.Item.Product.ID);
  50. }
  51. public override Filter<JobRequisitionItem>? DefineFilter(PurchaseOrderItemAllocation[] items)
  52. {
  53. var jobIDs = items.Select(x => x.Job.ID).Distinct().ToArray();
  54. if(jobIDs.Length > 1)
  55. {
  56. return new Filter<JobRequisitionItem>().None();
  57. }
  58. var jobID = jobIDs.FirstOrDefault();
  59. var productID = items.Select(x => x.Item.Product.ID).FirstOrDefault();
  60. if(productID == Guid.Empty)
  61. {
  62. return new Filter<JobRequisitionItem>().None();
  63. }
  64. return new Filter<JobRequisitionItem>(x => x.Job.ID).IsEqualTo(jobID)
  65. .And(x => x.Product.ID).IsEqualTo(productID);
  66. }
  67. public override Columns<JobRequisitionItem> DefineColumns()
  68. {
  69. return Columns.None<JobRequisitionItem>().Add(x => x.Job.JobNumber).Add(x => x.Requisition.Number).Add(x => x.Requisition.Description);
  70. }
  71. public override string FormatDisplay(CoreRow row)
  72. {
  73. var jobNumber = row.Get<JobRequisitionItem, string>(x => x.Job.JobNumber);
  74. var requiNumber = row.Get<JobRequisitionItem, int>(x => x.Requisition.Number);
  75. var requiDesc = row.Get<JobRequisitionItem, string>(x => x.Requisition.Description);
  76. return $"{jobNumber}: #{requiNumber} ({requiDesc})";
  77. }
  78. }
  79. /// <summary>
  80. /// This may be an empty link. The interface is as such: if there is no JRI, then we are creating a reserve and allocation just against the job. If there is a JRI,
  81. /// then received stock is reserved and allocated for the JRI.
  82. /// </summary>
  83. [EntityRelationship(DeleteAction.Cascade)]
  84. [LookupDefinition(typeof(JobRequisitionItemLookup))]
  85. [EditorSequence(2)]
  86. public JobRequisitionItemLink JobRequisitionItem { get; set; }
  87. [EditorSequence(3)]
  88. public double Quantity { get; set; }
  89. [NullEditor]
  90. public string PostedReference { get; set; }
  91. protected override void DoPropertyChanged(string name, object? before, object? after)
  92. {
  93. base.DoPropertyChanged(name, before, after);
  94. if(name == $"{nameof(Job)}.{nameof(Job.ID)}")
  95. {
  96. JobRequisitionItem.ID = Guid.Empty;
  97. JobRequisitionItem.Clear();
  98. }
  99. }
  100. }
  101. public class PurchaseOrderItemAllocationJobLink : EntityLink<PurchaseOrderItemAllocation>
  102. {
  103. [NullEditor]
  104. public override Guid ID { get; set; }
  105. public LightJobLink Job { get; set; }
  106. public LightPurchaseOrderItemLink Item { get; set; }
  107. }
  108. }