StockMovement.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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
  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. [CurrencyEditor(Visible = Visible.Default)]
  96. [EditorSequence(9)]
  97. public double Cost { get; set; } = 0.0;
  98. [EditorSequence(10)]
  99. [EntityRelationship(DeleteAction.SetNull)]
  100. public StockLocationLink Location { get; set; }
  101. [EditorSequence(11)]
  102. [EntityRelationship(DeleteAction.SetNull)]
  103. public JobLink Job { get; set; }
  104. [MemoEditor]
  105. [EditorSequence(12)]
  106. public string Notes { get; set; }
  107. [EditorSequence(13)]
  108. [EnumLookupEditor(typeof(StockMovementType), Visible = Visible.Default)]
  109. public StockMovementType Type { get; set; }
  110. [EditorSequence(14)]
  111. [EntityRelationship(DeleteAction.SetNull)]
  112. public EmployeeLink Employee { get; set; }
  113. /// <summary>
  114. /// To link StockMovements into conceptual blocks that cannot exist independently of each other; for example,
  115. /// an issue may require transfers in and out which are intrinsically tied to the issue.
  116. /// </summary>
  117. /// <remarks>
  118. /// <b>Important:</b> if this stock movement is a transfer, use <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
  119. /// </remarks>
  120. [NullEditor]
  121. public Guid Transaction { get; set; } = Guid.NewGuid();
  122. /// <summary>
  123. /// To link TransferOut/TransferIn together pairwise. <b>Only</b> edit via the Link* methods provided by <see cref="StockMovement"/>, such
  124. /// as <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
  125. /// </summary>
  126. [NullEditor]
  127. public Guid TransferID { get; set; } = Guid.NewGuid();
  128. [NullEditor]
  129. public bool System { get; set; }
  130. [NullEditor]
  131. [Obsolete("Replaced with Type", true)]
  132. public bool IsTransfer { get; set; } = false;
  133. [NullEditor]
  134. public PurchaseOrderItemLink OrderItem { get; set; }
  135. private class JobRequisitionItemLookup : LookupDefinitionGenerator<JobRequisitionItem, StockMovement>
  136. {
  137. public override Columns<JobRequisitionItem> DefineColumns()
  138. {
  139. return Columns.None<JobRequisitionItem>().Add(x => x.Job.JobNumber).Add(x => x.Requisition.Number).Add(x => x.Requisition.Description);
  140. }
  141. public override string FormatDisplay(CoreRow row)
  142. {
  143. var jobNumber = row.Get<JobRequisitionItem, string>(x => x.Job.JobNumber);
  144. var requiNumber = row.Get<JobRequisitionItem, int>(x => x.Requisition.Number);
  145. var requiDesc = row.Get<JobRequisitionItem, string>(x => x.Requisition.Description);
  146. return $"{jobNumber}: #{requiNumber} ({requiDesc})";
  147. }
  148. }
  149. [RequiredColumn]
  150. [LookupDefinition(typeof(JobRequisitionItemLookup))]
  151. public JobRequisitionItemLink JobRequisitionItem { get; set; }
  152. [NullEditor]
  153. public InvoiceLink Invoice { get; set; }
  154. private class JobScopeLookup : LookupDefinitionGenerator<JobScope, StockMovement>
  155. {
  156. public override Filter<JobScope> DefineFilter(StockMovement[] items)
  157. {
  158. var item = items?.Length == 1 ? items[0] : null;
  159. if (item != null)
  160. return Filter<JobScope>.Where(x => x.Job.ID).IsEqualTo(item.Job.ID).And(x => x.Status.Approved).IsEqualTo(true);
  161. return Filter.None<JobScope>();
  162. }
  163. public override Columns<StockMovement> DefineFilterColumns()
  164. => Columns.None<StockMovement>().Add(x=>x.Job.ID);
  165. }
  166. [LookupDefinition(typeof(JobScopeLookup))]
  167. [EditorSequence(5)]
  168. [EntityRelationship(DeleteAction.SetNull)]
  169. public JobScopeLink JobScope { get; set; }
  170. public ActualCharge Charge { get; set; }
  171. private class DocumentsCount : ComplexFormulaGenerator<StockMovement, int>
  172. {
  173. public override IComplexFormulaNode<StockMovement, int> GetFormula() =>
  174. Count<StockMovementBatchDocument, Guid>(
  175. x => x.Property(x => x.ID))
  176. .WithLink(x => x.EntityLink.ID, x => x.Batch.ID);
  177. }
  178. [ComplexFormula(typeof(DocumentsCount))]
  179. [NullEditor]
  180. public int Documents { get; set; }
  181. /// <summary>
  182. /// Used to Group together movements (particularly images)
  183. /// when transactions are uploaded from Mobile Devices
  184. /// </summary>
  185. [EntityRelationship(DeleteAction.Cascade)]
  186. public StockMovementBatchLink Batch { get; set; }
  187. [NullEditor]
  188. [Obsolete("Replaced with Dimensions", true)]
  189. public double UnitSize { get; set; }
  190. private class ValueFormula : ComplexFormulaGenerator<StockMovement, double>
  191. {
  192. public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
  193. Formula(FormulaOperator.Multiply,
  194. Property(x => x.Units),
  195. Property(x => x.Cost));
  196. }
  197. /// <summary>
  198. /// Value of a stock movement, equal to <see cref="Units"/> * <see cref="Cost"/>.
  199. /// </summary>
  200. [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary=Summary.Sum)]
  201. [ComplexFormula(typeof(ValueFormula))]
  202. public double Value { get; set; } = 0.0;
  203. [NullEditor]
  204. [LoggableProperty]
  205. public DateTime Posted { get; set; }
  206. [NullEditor]
  207. [LoggableProperty]
  208. [RequiredColumn]
  209. public PostedStatus PostedStatus { get; set; }
  210. [NullEditor]
  211. public string PostedNote { get; set; }
  212. [NullEditor]
  213. public string PostedReference { get; set; }
  214. /// <summary>
  215. /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferIn"/>. This
  216. /// will also set this movement to be <see cref="StockMovementType.TransferOut"/>.
  217. /// </summary>
  218. /// <remarks>
  219. /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
  220. /// uses that instead of <paramref name="other"/>.Transaction.
  221. /// </remarks>
  222. public void LinkAsTransferOut(StockMovement other, Guid? transaction = null)
  223. {
  224. if(other.Type != StockMovementType.TransferIn)
  225. {
  226. throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferIn)}", nameof(other));
  227. }
  228. else if(other.TransferID != Guid.Empty)
  229. {
  230. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
  231. }
  232. Transaction = transaction ?? other.Transaction;
  233. var transfer = Guid.NewGuid();
  234. other.TransferID = transfer;
  235. TransferID = transfer;
  236. Type = StockMovementType.TransferOut;
  237. }
  238. /// <summary>
  239. /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferOut"/>. This
  240. /// will also set this movement to be <see cref="StockMovementType.TransferIn"/>.
  241. /// </summary>
  242. /// <remarks>
  243. /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
  244. /// uses that instead of <paramref name="other"/>.Transaction.
  245. /// </remarks>
  246. public void LinkAsTransferIn(StockMovement other, Guid? transaction = null)
  247. {
  248. if(other.Type != StockMovementType.TransferOut)
  249. {
  250. throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferOut)}", nameof(other));
  251. }
  252. else if(other.TransferID != Guid.Empty)
  253. {
  254. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
  255. }
  256. Transaction = transaction ?? other.Transaction;
  257. var transfer = Guid.NewGuid();
  258. other.TransferID = transfer;
  259. TransferID = transfer;
  260. Type = StockMovementType.TransferIn;
  261. }
  262. /// <summary>
  263. /// Link the provided transfers together, and set them to have type <see cref="StockMovementType.TransferOut"/> and
  264. /// <see cref="StockMovementType.TransferIn"/>, respectively.
  265. /// </summary>
  266. /// <exception cref="ArgumentException">If either transfer has already been linked to a transfer.</exception>
  267. public static void LinkTransfers(StockMovement transferOut, StockMovement transferIn, Guid? transaction = null)
  268. {
  269. if(transferOut.TransferID != Guid.Empty)
  270. {
  271. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferOut));
  272. }
  273. else if(transferIn.TransferID != Guid.Empty)
  274. {
  275. throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferIn));
  276. }
  277. transferOut.Type = StockMovementType.TransferOut;
  278. if (transaction.HasValue)
  279. {
  280. transferOut.Transaction = transaction.Value;
  281. }
  282. transferIn.LinkAsTransferIn(transferOut, transaction);
  283. }
  284. static StockMovement()
  285. {
  286. StockEntity.LinkStockDimensions<StockMovement>();
  287. LinkedProperties.Register<StockMovement, ProductStyleLink, Guid>(x=>x.Product.DefaultInstance.Style, x => x.ID, x => x.Style.ID);
  288. //LinkedProperties.Register<StockMovement, ProductLink, double>(x => x.Product, x => x.AverageCost, x => x.Cost);
  289. LinkedProperties.Register<StockMovement, JobScopeLink, Guid>(ass => ass.Job.DefaultScope, scope => scope.ID, ass => ass.JobScope.ID);
  290. LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Number, ass => ass.JobScope.Number);
  291. LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Description, ass => ass.JobScope.Description);
  292. }
  293. private static Column<StockMovement> unitsize = new Column<StockMovement>(x => x.Dimensions.Value);
  294. private static Column<StockMovement> issued = new Column<StockMovement>(x => x.Issued);
  295. private static Column<StockMovement> received = new Column<StockMovement>(x => x.Received);
  296. private bool bChanging;
  297. protected override void DoPropertyChanged(string name, object? before, object? after)
  298. {
  299. if (bChanging)
  300. return;
  301. if (unitsize.IsEqualTo(name))
  302. {
  303. bChanging = true;
  304. Qty = (Received - Issued) * (double)after;
  305. bChanging = false;
  306. }
  307. else if (issued.IsEqualTo(name))
  308. {
  309. bChanging = true;
  310. Units = Received - (double)after;
  311. Qty = Units * Dimensions.Value;
  312. bChanging = false;
  313. }
  314. else if (received.IsEqualTo(name))
  315. {
  316. bChanging = true;
  317. Units = ((double)after - Issued);
  318. Qty = Units * Dimensions.Value;
  319. bChanging = false;
  320. }
  321. base.DoPropertyChanged(name, before, after);
  322. }
  323. }
  324. }