StockMovement.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Linq.Expressions;
  5. using InABox.Core;
  6. using PRSClasses;
  7. namespace Comal.Classes
  8. {
  9. public class StockMovementLink : EntityLink<StockMovement>
  10. {
  11. [NullEditor]
  12. public override Guid ID { get; set; }
  13. }
  14. [UserTracking("Warehousing")]
  15. public class StockMovement : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>,
  16. ILicense<WarehouseLicense>, IStockHolding, IJobMaterial, IExportable, IImportable, IPostable, IInvoiceable
  17. {
  18. [DateTimeEditor]
  19. [EditorSequence(0)]
  20. [SecondaryIndex]
  21. public DateTime Date { get; set; }
  22. private class ProductLookupGenerator : LookupDefinitionGenerator<Product, StockMovement>
  23. {
  24. public override Filter<Product>? DefineFilter(StockMovement[] items)
  25. => LookupFactory.DefineFilter<Product>().And(x => x.NonStock).IsEqualTo(false);
  26. }
  27. [EditorSequence(1)]
  28. [EntityRelationship(DeleteAction.Cascade)]
  29. [RequiredColumn]
  30. [LookupDefinition(typeof(ProductLookupGenerator))]
  31. public override ProductLink Product { get; set; }
  32. [EditorSequence(2)]
  33. [RequiredColumn]
  34. [DimensionsEditor(typeof(StockDimensions))]
  35. public override StockDimensions Dimensions { get; set; }
  36. [EditorSequence(3)]
  37. [EntityRelationship(DeleteAction.SetNull)]
  38. public ProductStyleLink Style { get; set; }
  39. // Allowed to be negative.
  40. [DoubleEditor(Summary = Summary.Sum)]
  41. [EditorSequence(4)]
  42. public double Received { get; set; }
  43. [DoubleEditor(Summary = Summary.Sum)]
  44. [EditorSequence(5)]
  45. public double Issued { get; set; }
  46. /// <summary>
  47. /// This indicates the balance of the holding at the current point in time for a stock movement, populated for stock takes.
  48. /// </summary>
  49. [DoubleEditor]
  50. [EditorSequence(6)]
  51. public double Balance { get; set; }
  52. private class StockMovementUnitsFormula : ComplexFormulaGenerator<StockMovement, double>
  53. {
  54. public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
  55. Formula(FormulaOperator.Subtract, Property(x => x.Received), Property(x => x.Issued));
  56. }
  57. /// <summary>
  58. /// Units = Received - Issued
  59. /// </summary>
  60. [ComplexFormula(typeof(StockMovementUnitsFormula))]
  61. [EditorSequence(7)]
  62. [DoubleEditor(Visible=Visible.Optional, Editable = Editable.Hidden, Summary= Summary.Sum)]
  63. public double Units { get; set; }
  64. private class IsRemnantCondition : ComplexFormulaGenerator<StockMovement, bool>
  65. {
  66. public override IComplexFormulaNode<StockMovement, bool> GetFormula() =>
  67. If<double>(
  68. x => x.Property(x => x.Dimensions.Value),
  69. Condition.LessThan,
  70. x => x.Property(x => x.Product.DefaultInstance.Dimensions.Value))
  71. .Then(Constant(true))
  72. .Else(Constant(false));
  73. }
  74. /// <summary>
  75. /// IsRemnant = Dimensions.Value &lt; Product.Dimensions.Value
  76. /// </summary>
  77. [CheckBoxEditor(Editable = Editable.Hidden)]
  78. [ComplexFormula(typeof(IsRemnantCondition))]
  79. [EditorSequence(7)]
  80. public bool IsRemnant { get; set; }
  81. private class QuantityFormula : ComplexFormulaGenerator<StockMovement, double>
  82. {
  83. public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
  84. Formula(FormulaOperator.Multiply,
  85. Property(x => x.Units),
  86. Property(x => x.Dimensions.Value));
  87. }
  88. /// <summary>
  89. /// Qty = Units * Dimensions.Value
  90. /// </summary>
  91. [EditorSequence(8)]
  92. [ComplexFormula(typeof(QuantityFormula))]
  93. [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
  94. public double Qty { get; set; }
  95. /// <summary>
  96. /// Cost per unit, not cost per Dimensions.Value, so that Value = Units * Cost
  97. /// </summary>
  98. [CurrencyEditor(Visible = Visible.Default)]
  99. [EditorSequence(9)]
  100. public double Cost { get; set; } = 0.0;
  101. [EditorSequence(10)]
  102. [EntityRelationship(DeleteAction.SetNull)]
  103. public StockLocationLink Location { get; set; }
  104. [EditorSequence(11)]
  105. [EntityRelationship(DeleteAction.SetNull)]
  106. public JobLink Job { get; set; }
  107. [MemoEditor]
  108. [EditorSequence(12)]
  109. public string Notes { get; set; }
  110. [EditorSequence(13)]
  111. [EnumLookupEditor(typeof(StockMovementType), Visible = Visible.Default)]
  112. public StockMovementType Type { get; set; }
  113. [EditorSequence(14)]
  114. [EntityRelationship(DeleteAction.SetNull)]
  115. public EmployeeLink Employee { get; set; }
  116. /// <summary>
  117. /// To link StockMovements into conceptual blocks that cannot exist independently of each other; for example,
  118. /// an issue may require transfers in and out which are intrinsically tied to the issue.
  119. /// </summary>
  120. /// <remarks>
  121. /// <b>Important:</b> if this stock movement is a transfer, use <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
  122. /// </remarks>
  123. [NullEditor]
  124. public Guid Transaction { get; set; } = Guid.NewGuid();
  125. /// <summary>
  126. /// To link TransferOut/TransferIn together pairwise. <b>Only</b> edit via the Link* methods provided by <see cref="StockMovement"/>, such
  127. /// as <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
  128. /// </summary>
  129. [NullEditor]
  130. public Guid TransferID { get; set; }
  131. [NullEditor]
  132. public bool System { get; set; }
  133. [NullEditor]
  134. [Obsolete("Replaced with Type", true)]
  135. public bool IsTransfer { get; set; } = false;
  136. [NullEditor]
  137. public PurchaseOrderItemLink OrderItem { get; set; }
  138. private class JobRequisitionItemLookup : LookupDefinitionGenerator<JobRequisitionItem, StockMovement>
  139. {
  140. public override Columns<JobRequisitionItem> DefineColumns()
  141. {
  142. return Columns.None<JobRequisitionItem>().Add(x => x.Job.JobNumber).Add(x => x.Requisition.Number).Add(x => x.Requisition.Description);
  143. }
  144. public override string FormatDisplay(CoreRow row)
  145. {
  146. var jobNumber = row.Get<JobRequisitionItem, string>(x => x.Job.JobNumber);
  147. var requiNumber = row.Get<JobRequisitionItem, int>(x => x.Requisition.Number);
  148. var requiDesc = row.Get<JobRequisitionItem, string>(x => x.Requisition.Description);
  149. return $"{jobNumber}: #{requiNumber} ({requiDesc})";
  150. }
  151. }
  152. [RequiredColumn]
  153. [LookupDefinition(typeof(JobRequisitionItemLookup))]
  154. public JobRequisitionItemLink JobRequisitionItem { get; set; }
  155. [NullEditor]
  156. public InvoiceLink Invoice { get; set; }
  157. private class JobScopeLookup : LookupDefinitionGenerator<JobScope, StockMovement>
  158. {
  159. public override Filter<JobScope> DefineFilter(StockMovement[] items)
  160. {
  161. var item = items?.Length == 1 ? items[0] : null;
  162. if (item != null)
  163. return Filter<JobScope>.Where(x => x.Job.ID).IsEqualTo(item.Job.ID).And(x => x.Status.Approved).IsEqualTo(true);
  164. return Filter.None<JobScope>();
  165. }
  166. public override Columns<StockMovement> DefineFilterColumns()
  167. => Columns.None<StockMovement>().Add(x=>x.Job.ID);
  168. }
  169. [LookupDefinition(typeof(JobScopeLookup))]
  170. [EditorSequence(5)]
  171. [EntityRelationship(DeleteAction.SetNull)]
  172. public JobScopeLink JobScope { get; set; }
  173. public ActualCharge Charge { get; set; }
  174. private class DocumentsCount : ComplexFormulaGenerator<StockMovement, int>
  175. {
  176. public override IComplexFormulaNode<StockMovement, int> GetFormula() =>
  177. Count<StockMovementBatchDocument, Guid>(
  178. x => x.Property(x => x.ID))
  179. .WithLink(x => x.Entity.ID, x => x.Batch.ID);
  180. }
  181. [ComplexFormula(typeof(DocumentsCount))]
  182. [NullEditor]
  183. public int Documents { get; set; }
  184. /// <summary>
  185. /// Used to Group together movements (particularly images)
  186. /// when transactions are uploaded from Mobile Devices
  187. /// </summary>
  188. [EntityRelationship(DeleteAction.Cascade)]
  189. public StockMovementBatchLink Batch { get; set; }
  190. [NullEditor]
  191. [Obsolete("Replaced with Dimensions", true)]
  192. public double UnitSize { get; set; }
  193. private class ValueFormula : ComplexFormulaGenerator<StockMovement, double>
  194. {
  195. public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
  196. Formula(FormulaOperator.Multiply,
  197. Property(x => x.Units),
  198. Property(x => x.Cost));
  199. }
  200. /// <summary>
  201. /// Value of a stock movement, equal to <see cref="Units"/> * <see cref="Cost"/>.
  202. /// </summary>
  203. [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary=Summary.Sum)]
  204. [ComplexFormula(typeof(ValueFormula))]
  205. public double Value { get; set; } = 0.0;
  206. [NullEditor]
  207. [LoggableProperty]
  208. public DateTime Posted { get; set; }
  209. [NullEditor]
  210. [LoggableProperty]
  211. [RequiredColumn]
  212. public PostedStatus PostedStatus { get; set; }
  213. [NullEditor]
  214. public string PostedNote { get; set; }
  215. [NullEditor]
  216. public string PostedReference { get; set; }
  217. /// <summary>
  218. /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferIn"/>. This
  219. /// will also set this movement to be <see cref="StockMovementType.TransferOut"/>.
  220. /// </summary>
  221. /// <remarks>
  222. /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
  223. /// uses that instead of <paramref name="other"/>.Transaction.
  224. /// </remarks>
  225. public void LinkAsTransferOut(StockMovement other, Guid? transaction = null)
  226. {
  227. if(other.Type != StockMovementType.TransferIn)
  228. {
  229. throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferIn)}", nameof(other));
  230. }
  231. else if(other.TransferID != Guid.Empty)
  232. {
  233. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
  234. }
  235. Transaction = transaction ?? other.Transaction;
  236. var transfer = Guid.NewGuid();
  237. other.TransferID = transfer;
  238. TransferID = transfer;
  239. Type = StockMovementType.TransferOut;
  240. }
  241. /// <summary>
  242. /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferOut"/>. This
  243. /// will also set this movement to be <see cref="StockMovementType.TransferIn"/>.
  244. /// </summary>
  245. /// <remarks>
  246. /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
  247. /// uses that instead of <paramref name="other"/>.Transaction.
  248. /// </remarks>
  249. public void LinkAsTransferIn(StockMovement other, Guid? transaction = null)
  250. {
  251. if(other.Type != StockMovementType.TransferOut)
  252. {
  253. throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferOut)}", nameof(other));
  254. }
  255. else if(other.TransferID != Guid.Empty)
  256. {
  257. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
  258. }
  259. Transaction = transaction ?? other.Transaction;
  260. var transfer = Guid.NewGuid();
  261. other.TransferID = transfer;
  262. TransferID = transfer;
  263. Type = StockMovementType.TransferIn;
  264. }
  265. /// <summary>
  266. /// Link the provided transfers together, and set them to have type <see cref="StockMovementType.TransferOut"/> and
  267. /// <see cref="StockMovementType.TransferIn"/>, respectively.
  268. /// </summary>
  269. /// <exception cref="ArgumentException">If either transfer has already been linked to a transfer.</exception>
  270. public static void LinkTransfers(StockMovement transferOut, StockMovement transferIn, Guid? transaction = null)
  271. {
  272. if(transferOut.TransferID != Guid.Empty)
  273. {
  274. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferOut));
  275. }
  276. else if(transferIn.TransferID != Guid.Empty)
  277. {
  278. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferIn));
  279. }
  280. transferOut.Type = StockMovementType.TransferOut;
  281. if (transaction.HasValue)
  282. {
  283. transferOut.Transaction = transaction.Value;
  284. }
  285. transferIn.LinkAsTransferIn(transferOut, transaction);
  286. }
  287. static StockMovement()
  288. {
  289. StockEntity.LinkStockDimensions<StockMovement>();
  290. LinkedProperties.Register<StockMovement, ProductStyleLink, Guid>(x=>x.Product.DefaultInstance.Style, x => x.ID, x => x.Style.ID);
  291. //LinkedProperties.Register<StockMovement, ProductLink, double>(x => x.Product, x => x.AverageCost, x => x.Cost);
  292. LinkedProperties.Register<StockMovement, JobScopeLink, Guid>(ass => ass.Job.DefaultScope, scope => scope.ID, ass => ass.JobScope.ID);
  293. LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Number, ass => ass.JobScope.Number);
  294. LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Description, ass => ass.JobScope.Description);
  295. }
  296. private static Column<StockMovement> unitsize = new Column<StockMovement>(x => x.Dimensions.Value);
  297. private static Column<StockMovement> issued = new Column<StockMovement>(x => x.Issued);
  298. private static Column<StockMovement> received = new Column<StockMovement>(x => x.Received);
  299. private bool bChanging;
  300. protected override void DoPropertyChanged(string name, object? before, object? after)
  301. {
  302. if (bChanging)
  303. return;
  304. if (unitsize.IsEqualTo(name))
  305. {
  306. bChanging = true;
  307. Qty = (Received - Issued) * (double)after;
  308. bChanging = false;
  309. }
  310. else if (issued.IsEqualTo(name))
  311. {
  312. bChanging = true;
  313. Units = Received - (double)after;
  314. Qty = Units * Dimensions.Value;
  315. bChanging = false;
  316. }
  317. else if (received.IsEqualTo(name))
  318. {
  319. bChanging = true;
  320. Units = ((double)after - Issued);
  321. Qty = Units * Dimensions.Value;
  322. bChanging = false;
  323. }
  324. base.DoPropertyChanged(name, before, after);
  325. }
  326. }
  327. }