|
@@ -0,0 +1,332 @@
|
|
|
|
|
+using System;
|
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
|
+using System.ComponentModel;
|
|
|
|
|
+using System.Linq;
|
|
|
|
|
+using PX.Common;
|
|
|
|
|
+using PX.Data;
|
|
|
|
|
+using PX.Objects.CM;
|
|
|
|
|
+using PX.Objects.GL;
|
|
|
|
|
+using PX.Objects.IN;
|
|
|
|
|
+using PX.Objects.PM;
|
|
|
|
|
+
|
|
|
|
|
+namespace ProjectEntryLaborAdjust
|
|
|
|
|
+{
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public class ProjectEntryExt : PXGraphExtension<ProjectEntry>
|
|
|
|
|
+ {
|
|
|
|
|
+ private const string SlotLaborUpdate = "RevBudSetLabor"; // <-- new flag
|
|
|
|
|
+ private const string LaborInvCD = "L023";
|
|
|
|
|
+ private const string ExcludedBranchCD = "301";
|
|
|
|
|
+ private const string ExcludedBillRule = "PROGRESS";
|
|
|
|
|
+ private const decimal PMRate = 90m;
|
|
|
|
|
+ private int? _laborInvID;
|
|
|
|
|
+ private int? LaborInvID
|
|
|
|
|
+ {
|
|
|
|
|
+ get
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_laborInvID == null)
|
|
|
|
|
+ _laborInvID = InventoryItem.UK.Find(Base, LaborInvCD)?.InventoryID;
|
|
|
|
|
+ return _laborInvID;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected bool Skip => Base.IsImport || Base.IsContractBasedAPI || Base.IsCopyPasteContext;
|
|
|
|
|
+
|
|
|
|
|
+ protected void _(Events.RowUpdated<PMRevenueBudget> e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (IsExcludedProject()) // ← add this line
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ var row = (PMRevenueBudget)e.Row;
|
|
|
|
|
+ var old = (PMRevenueBudget)e.OldRow;
|
|
|
|
|
+ if (row == null || old == null || Skip) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Prevent recursion
|
|
|
|
|
+ if (PXContext.GetSlot<bool?>(SlotLaborUpdate) == true) return;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // Only react when Original Budgeted Amount changed
|
|
|
|
|
+ if (e.Cache.ObjectsEqual<PMRevenueBudget.curyAmount>(row, old))
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Ignore if user is editing the labor line itself
|
|
|
|
|
+
|
|
|
|
|
+ if (LaborInvID != null && row.InventoryID == LaborInvID) return;
|
|
|
|
|
+
|
|
|
|
|
+ // ---- Calculate totals/hours (exclude labor line) -----------------
|
|
|
|
|
+ decimal totalRevised = GetRevisedRevenueTotalUI(LaborInvCD);
|
|
|
|
|
+ decimal pmHours = GetPMHours(totalRevised);
|
|
|
|
|
+
|
|
|
|
|
+ PXContext.SetSlot<bool?>(SlotLaborUpdate, true);
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ var cache = Base.RevenueBudget.Cache;
|
|
|
|
|
+ PMRevenueBudget laborLine = FindRevenueLineByInventoryCd(LaborInvCD);
|
|
|
|
|
+
|
|
|
|
|
+ if (laborLine != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ var c = Base.RevenueBudget.Cache;
|
|
|
|
|
+
|
|
|
|
|
+ if (laborLine.Qty != pmHours)
|
|
|
|
|
+ c.SetValueExt<PMRevenueBudget.qty>(laborLine, pmHours);
|
|
|
|
|
+
|
|
|
|
|
+ if (laborLine.CuryUnitRate != PMRate)
|
|
|
|
|
+ c.SetValueExt<PMRevenueBudget.curyUnitRate>(laborLine, PMRate);
|
|
|
|
|
+
|
|
|
|
|
+ // DO NOT set curyAmount – let the formula do it.
|
|
|
|
|
+ c.Update(laborLine);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ CreateRevenueBudgetLine(
|
|
|
|
|
+ inventoryCD: LaborInvCD,
|
|
|
|
|
+ taskID: row.ProjectTaskID,
|
|
|
|
|
+ accountGroupID: row.AccountGroupID,
|
|
|
|
|
+ qty: pmHours,
|
|
|
|
|
+ rate: PMRate,
|
|
|
|
|
+ amount: pmHours * PMRate,
|
|
|
|
|
+ uom: row.UOM,
|
|
|
|
|
+ description: "Auto-added PM Labor"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ finally
|
|
|
|
|
+ {
|
|
|
|
|
+ PXContext.SetSlot<bool?>(SlotLaborUpdate, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- Popup & save -----------------------------------
|
|
|
|
|
+ /*ShowPopupOnce(totalOrig);*/
|
|
|
|
|
+ // Base.Actions.PressSave();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected void _(Events.RowPersisting<PMRevenueBudget> e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (IsExcludedProject()) // ← add this line
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ var row = (PMRevenueBudget)e.Row;
|
|
|
|
|
+ if (row == null || e.Operation == PXDBOperation.Delete) return;
|
|
|
|
|
+ if (Skip || PXContext.GetSlot<bool?>(SlotLaborUpdate) == true) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (LaborInvID != null && row.InventoryID == LaborInvID)
|
|
|
|
|
+ {
|
|
|
|
|
+ // ensure both currency and base rates are correct right before save
|
|
|
|
|
+ decimal baseRate;
|
|
|
|
|
+ PXCurrencyAttribute.CuryConvBase<PMRevenueBudget.curyUnitRate>(e.Cache, row, PMRate, out baseRate);
|
|
|
|
|
+
|
|
|
|
|
+ if (row.CuryUnitRate != PMRate)
|
|
|
|
|
+ e.Cache.SetValue<PMRevenueBudget.curyUnitRate>(row, PMRate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private bool IsExcludedProject()
|
|
|
|
|
+ {
|
|
|
|
|
+ PMProject proj = Base.Project.Current;
|
|
|
|
|
+ if (proj == null) return false;
|
|
|
|
|
+
|
|
|
|
|
+ // Branch check (BranchCD = 301) --------------------------------
|
|
|
|
|
+ if (proj.DefaultBranchID != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Branch br = PXSelect<Branch,
|
|
|
|
|
+ Where<Branch.branchID, Equal<Required<Branch.branchID>>>>
|
|
|
|
|
+ .Select(Base, proj.DefaultBranchID);
|
|
|
|
|
+ if (br?.BranchCD?.Trim() == ExcludedBranchCD)
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Billing‑rule check (BillingRule = PROGRESS) ------------------
|
|
|
|
|
+ // Field name differs by Acumatica build; common ones shown.
|
|
|
|
|
+ string billRule = proj.BillingID; // e.g., 2021 R1+
|
|
|
|
|
+
|
|
|
|
|
+ if (!string.IsNullOrEmpty(billRule) &&
|
|
|
|
|
+ string.Equals(billRule.Trim(), ExcludedBillRule,
|
|
|
|
|
+ StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ return true;
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public static bool IsActive()
|
|
|
|
|
+ {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ protected void _(Events.FieldUpdated<PMRevenueBudget, PMRevenueBudget.curyUnitRate> e)
|
|
|
|
|
+ {
|
|
|
|
|
+ var r = (PMRevenueBudget)e.Row;
|
|
|
|
|
+ if (r == null) return;
|
|
|
|
|
+ PXTrace.WriteInformation($"curyUnitRate UPDATED -> {r.CuryUnitRate} (Inv={r.InventoryID})");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private PMRevenueBudget FindRevenueLineByInventoryCd(string inventoryCD)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (string.IsNullOrEmpty(inventoryCD))
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ // Resolve InventoryID from the CD
|
|
|
|
|
+ InventoryItem inv = InventoryItem.UK.Find(Base, inventoryCD);
|
|
|
|
|
+ if (inv == null) return null;
|
|
|
|
|
+
|
|
|
|
|
+ int? projID = Base.Project.Current?.ContractID;
|
|
|
|
|
+ if (projID == null) return null;
|
|
|
|
|
+
|
|
|
|
|
+ // 1) Cache (includes unsaved rows already on screen)
|
|
|
|
|
+ var cached = Base.RevenueBudget.Cache.Cached
|
|
|
|
|
+ .Cast<PMRevenueBudget>()
|
|
|
|
|
+ .FirstOrDefault(r => r.ProjectID == projID
|
|
|
|
|
+ && r.InventoryID == inv.InventoryID
|
|
|
|
|
+ && r.Type == AccountType.Income);
|
|
|
|
|
+ if (cached != null) return cached;
|
|
|
|
|
+
|
|
|
|
|
+ // 2) DB (readonly, excludes unsaved edits)
|
|
|
|
|
+ var db = PXSelectReadonly<PMRevenueBudget,
|
|
|
|
|
+ Where<PMRevenueBudget.projectID, Equal<Required<PMRevenueBudget.projectID>>,
|
|
|
|
|
+ And<PMRevenueBudget.inventoryID, Equal<Required<PMRevenueBudget.inventoryID>>,
|
|
|
|
|
+ And<PMRevenueBudget.type, Equal<AccountType.income>>>>>
|
|
|
|
|
+ .Select(Base, projID, inv.InventoryID)
|
|
|
|
|
+ .RowCast<PMRevenueBudget>()
|
|
|
|
|
+ .FirstOrDefault();
|
|
|
|
|
+
|
|
|
|
|
+ return db;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public PMRevenueBudget CreateRevenueBudgetLine(
|
|
|
|
|
+ int? inventoryID = null, string inventoryCD = null,
|
|
|
|
|
+ int? taskID = null, string taskCD = null,
|
|
|
|
|
+ int? accountGroupID = null, string accountGroupCD = null,
|
|
|
|
|
+ decimal? qty = null,
|
|
|
|
|
+ decimal? rate = null,
|
|
|
|
|
+ decimal? amount = null, // if null, amount = qty * rate (when both provided)
|
|
|
|
|
+ string uom = null,
|
|
|
|
|
+ string description = null,
|
|
|
|
|
+ int? costCodeID = null // optional
|
|
|
|
|
+ )
|
|
|
|
|
+ {
|
|
|
|
|
+ // Resolve keys ----------------------------------------------------
|
|
|
|
|
+ int? projectID = Base.Project.Current?.ContractID;
|
|
|
|
|
+ if (projectID == null)
|
|
|
|
|
+ throw new PXException();
|
|
|
|
|
+
|
|
|
|
|
+ if (inventoryID == null && !string.IsNullOrEmpty(inventoryCD))
|
|
|
|
|
+ inventoryID = InventoryItem.UK.Find(Base, inventoryCD)?.InventoryID;
|
|
|
|
|
+
|
|
|
|
|
+ if (taskID == null && !string.IsNullOrEmpty(taskCD))
|
|
|
|
|
+ taskID = PXSelect<PMTask,
|
|
|
|
|
+ Where<PMTask.projectID, Equal<Required<PMTask.projectID>>,
|
|
|
|
|
+ And<PMTask.taskCD, Equal<Required<PMTask.taskCD>>>>>
|
|
|
|
|
+ .Select(Base, projectID, taskCD)
|
|
|
|
|
+ ?.FirstOrDefault()?.GetItem<PMTask>()?.TaskID;
|
|
|
|
|
+
|
|
|
|
|
+ if (accountGroupID == null && !string.IsNullOrEmpty(accountGroupCD))
|
|
|
|
|
+ accountGroupID = PMAccountGroup.UK.Find(Base, accountGroupCD)?.GroupID;
|
|
|
|
|
+
|
|
|
|
|
+ if (qty == null) qty = 0m;
|
|
|
|
|
+ if (rate == null) rate = 0m;
|
|
|
|
|
+ if (amount == null) amount = qty * rate;
|
|
|
|
|
+
|
|
|
|
|
+ // Insert skeleton row --------------------------------------------
|
|
|
|
|
+ var cache = Base.RevenueBudget.Cache;
|
|
|
|
|
+ var line = new PMRevenueBudget
|
|
|
|
|
+ {
|
|
|
|
|
+ ProjectID = projectID,
|
|
|
|
|
+ Type = AccountType.Income // revenue
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ line = (PMRevenueBudget)cache.Insert(line);
|
|
|
|
|
+
|
|
|
|
|
+ // Set fields via SetValueExt so defaults/validations run ----------
|
|
|
|
|
+ if (taskID != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.projectTaskID>(line, taskID);
|
|
|
|
|
+
|
|
|
|
|
+ if (accountGroupID != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.accountGroupID>(line, accountGroupID);
|
|
|
|
|
+
|
|
|
|
|
+ if (inventoryID != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.inventoryID>(line, inventoryID);
|
|
|
|
|
+
|
|
|
|
|
+ if (costCodeID != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.costCodeID>(line, costCodeID);
|
|
|
|
|
+
|
|
|
|
|
+ if (!string.IsNullOrEmpty(uom))
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.uOM>(line, uom);
|
|
|
|
|
+
|
|
|
|
|
+ if (qty != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.qty>(line, qty);
|
|
|
|
|
+
|
|
|
|
|
+ if (rate != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.curyUnitRate>(line, rate);
|
|
|
|
|
+
|
|
|
|
|
+ if (amount != null)
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.curyAmount>(line, amount);
|
|
|
|
|
+
|
|
|
|
|
+ if (!string.IsNullOrEmpty(description))
|
|
|
|
|
+ cache.SetValueExt<PMRevenueBudget.description>(line, description);
|
|
|
|
|
+
|
|
|
|
|
+ // Final update so cache/state is consistent
|
|
|
|
|
+ line = (PMRevenueBudget)cache.Update(line);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ return line;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private decimal GetPMHours(decimal jobSize)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (jobSize < 0m) return 0m; // or throw
|
|
|
|
|
+
|
|
|
|
|
+ switch (jobSize)
|
|
|
|
|
+ {
|
|
|
|
|
+ case var n when n <= 1000m: return 1.0m;
|
|
|
|
|
+ case var n when n <= 2500m: return 1.5m;
|
|
|
|
|
+ case var n when n <= 5000m: return 3.0m;
|
|
|
|
|
+ case var n when n <= 7500m: return 3.5m;
|
|
|
|
|
+ case var n when n <= 10000m: return 5.0m;
|
|
|
|
|
+ case var n when n <= 25000m: return 6.0m;
|
|
|
|
|
+ case var n when n <= 50000m: return 9.0m;
|
|
|
|
|
+ default: return 10.5m;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private decimal GetRevisedRevenueTotalUI(params string[] excludeCDs)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Resolve the IDs to skip
|
|
|
|
|
+ var skip = new HashSet<int?>(
|
|
|
|
|
+ (excludeCDs ?? new string[0])
|
|
|
|
|
+ .Select(cd => InventoryItem.UK.Find(Base, cd)?.InventoryID)
|
|
|
|
|
+ .Where(id => id != null)
|
|
|
|
|
+ .Cast<int?>()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ decimal total = 0m;
|
|
|
|
|
+
|
|
|
|
|
+ // Select() returns rows currently in cache + remaining DB rows for the view
|
|
|
|
|
+ foreach (PMRevenueBudget line in Base.RevenueBudget.Select().RowCast<PMRevenueBudget>())
|
|
|
|
|
+ {
|
|
|
|
|
+ if (line.Type == AccountType.Income && !skip.Contains(line.InventoryID))
|
|
|
|
|
+ total += line.CuryRevisedAmount ?? 0m; // or CuryAmount if you want "original"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return total;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+}
|