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 { 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 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(SlotLaborUpdate) == true) return; // Only react when Original Budgeted Amount changed if (e.Cache.ObjectsEqual(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(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(laborLine, pmHours); if (laborLine.CuryUnitRate != PMRate) c.SetValueExt(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(SlotLaborUpdate, false); } // ---- Popup & save ----------------------------------- /*ShowPopupOnce(totalOrig);*/ // Base.Actions.PressSave(); } protected void _(Events.RowPersisting e) { if (IsExcludedProject()) // ← add this line return; var row = (PMRevenueBudget)e.Row; if (row == null || e.Operation == PXDBOperation.Delete) return; if (Skip || PXContext.GetSlot(SlotLaborUpdate) == true) return; if (LaborInvID != null && row.InventoryID == LaborInvID) { // ensure both currency and base rates are correct right before save decimal baseRate; PXCurrencyAttribute.CuryConvBase(e.Cache, row, PMRate, out baseRate); if (row.CuryUnitRate != PMRate) e.Cache.SetValue(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>>> .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 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() .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>, And>, And>>>> .Select(Base, projID, inv.InventoryID) .RowCast() .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>, And>>>> .Select(Base, projectID, taskCD) ?.FirstOrDefault()?.GetItem()?.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(line, taskID); if (accountGroupID != null) cache.SetValueExt(line, accountGroupID); if (inventoryID != null) cache.SetValueExt(line, inventoryID); if (costCodeID != null) cache.SetValueExt(line, costCodeID); if (!string.IsNullOrEmpty(uom)) cache.SetValueExt(line, uom); if (qty != null) cache.SetValueExt(line, qty); if (rate != null) cache.SetValueExt(line, rate); if (amount != null) cache.SetValueExt(line, amount); if (!string.IsNullOrEmpty(description)) cache.SetValueExt(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( (excludeCDs ?? new string[0]) .Select(cd => InventoryItem.UK.Find(Base, cd)?.InventoryID) .Where(id => id != null) .Cast() ); decimal total = 0m; // Select() returns rows currently in cache + remaining DB rows for the view foreach (PMRevenueBudget line in Base.RevenueBudget.Select().RowCast()) { if (line.Type == AccountType.Income && !skip.Contains(line.InventoryID)) total += line.CuryRevisedAmount ?? 0m; // or CuryAmount if you want "original" } return total; } } }