| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- using PX.Common;
- using PX.Data;
- using PX.Objects.GL;
- using PX.Objects.IN;
- using PX.Objects.PM;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- namespace ProjectEntryLaborAdjust
- {
- // ===================== GRAPH EXTENSION (RULE-DRIVEN) =====================
- public class ProjectEntryExt : PXGraphExtension<ProjectEntry>
- {
- private const string SlotLaborUpdate = "RevBudSetLabor";
- // List of rules to process — add new classes here in Initialize()
- private readonly List<LaborRuleBase> _rules = new List<LaborRuleBase>();
- // Cache InventoryID lookups for rule CDs
- private readonly Dictionary<string, int?> _invIdCache =
- new Dictionary<string, int?>(StringComparer.OrdinalIgnoreCase);
- protected bool Skip => Base.IsImport || Base.IsContractBasedAPI || Base.IsCopyPasteContext;
- public override void Initialize()
- {
- base.Initialize();
- _rules.Clear();
- _rules.Add(new L023PmLaborRule()); // uses AR pricing (no fixed rate)
- //_rules.Add(new L014LaborRule()); // fixed rate example
- // _rules.Add(new L001LaborRule());
- }
- // -------------------- Event Handlers --------------------
- protected void _(Events.RowUpdated<PMRevenueBudget> e)
- {
- var row = (PMRevenueBudget)e.Row;
- var old = (PMRevenueBudget)e.OldRow;
- if (row == null || old == null || Skip) return;
- if (PXContext.GetSlot<bool?>(SlotLaborUpdate) == true) return;
- // Only react when Original Budgeted Amount changed
- if (e.Cache.ObjectsEqual<PMRevenueBudget.curyAmount>(row, old)) return;
- // Build once
- string[] laborCDs = _rules.Select(r => r.InventoryCD).ToArray();
- int?[] laborIDs = laborCDs.Select(GetInventoryIdCached).ToArray();
- // If editing any labor line, skip to avoid recursion
- if (row.InventoryID != null && laborIDs.Contains(row.InventoryID))
- return;
- // Exclude ALL labor lines from the job-size total
- decimal totalRevised = GetRevisedRevenueTotalUI(laborCDs);
- // Resolve project context for per-rule exclusions
- PMProject proj = Base.Project.Current;
- string branchCD = ResolveBranchCD(proj?.DefaultBranchID);
- string templateCD = ResolveTemplateCD(proj?.TemplateID);
- string billRule = ResolveBillRule(proj);
- DateTime? projectCreatedDate = proj?.CreatedDateTime;
- PXContext.SetSlot<bool?>(SlotLaborUpdate, true);
- try
- {
- foreach (var rule in _rules)
- {
- // Per-rule exclusion check
- if (rule.IsExcludedFor(branchCD, templateCD, billRule, projectCreatedDate))
- {
- continue;
- }
- decimal hours = rule.GetHours(totalRevised, projectCreatedDate); ;
- UpsertLaborLine(rule, hours, row);
- }
- }
- finally
- {
- PXContext.SetSlot<bool?>(SlotLaborUpdate, false);
- }
- }
- // Re-assert fixed rates right before save
- protected void _(Events.RowPersisting<PMRevenueBudget> e)
- {
- var row = (PMRevenueBudget)e.Row;
- if (row == null || e.Operation == PXDBOperation.Delete) return;
- if (Skip || PXContext.GetSlot<bool?>(SlotLaborUpdate) == true) return;
- // if this row corresponds to a rule WITH a fixed rate, ensure the rate is set
- foreach (var rule in _rules)
- {
- var invID = GetInventoryIdCached(rule.InventoryCD);
- if (invID == null) continue;
- if (row.InventoryID == invID && rule.FixedUnitRate.HasValue)
- {
- decimal desiredRate = rule.FixedUnitRate.Value;
- // Using SetValue (not SetValueExt) at persist-time avoids extra field events
- if (row.CuryUnitRate != desiredRate)
- e.Cache.SetValue<PMRevenueBudget.curyUnitRate>(row, desiredRate);
- break; // at most one match
- }
- }
- }
- 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})");
- }
- // -------------------- Core Helpers --------------------
- private int? GetInventoryIdCached(string inventoryCD)
- {
- if (string.IsNullOrWhiteSpace(inventoryCD)) return null;
- int? cached;
- if (_invIdCache.TryGetValue(inventoryCD, out cached))
- return cached;
- var id = InventoryItem.UK.Find(Base, inventoryCD)?.InventoryID;
- _invIdCache[inventoryCD] = id;
- return id;
- }
- private void UpsertLaborLine(LaborRuleBase rule, decimal hours, PMRevenueBudget contextRow)
- {
- var cache = Base.RevenueBudget.Cache;
- var laborLine = FindRevenueLineByInventoryCd(rule.InventoryCD);
- if (laborLine != null)
- {
- bool qtyChanged = laborLine.Qty != hours;
- if (qtyChanged)
- cache.SetValueExt<PMRevenueBudget.qty>(laborLine, hours);
- // Rate logic (safe, minimal events):
- if (rule.FixedUnitRate.HasValue)
- {
- // Explicit fixed rate supplied by the rule
- decimal desiredRate = rule.FixedUnitRate.Value;
- if (laborLine.CuryUnitRate != desiredRate)
- cache.SetValueExt<PMRevenueBudget.curyUnitRate>(laborLine, desiredRate);
- }
- else if (qtyChanged)
- {
- // No explicit rate: re-apply pricing defaults when qty changed
- cache.SetDefaultExt<PMRevenueBudget.curyUnitRate>(laborLine);
- }
- cache.Update(laborLine);
- }
- else
- {
- // Creating a new line — pass rate if provided, and compute amount if we have both qty & rate
- decimal? rate = rule.FixedUnitRate;
- decimal? amount = rate.HasValue ? (decimal?)(hours * rate.Value) : null;
- CreateRevenueBudgetLine(
- inventoryID: null,
- inventoryCD: rule.InventoryCD,
- taskID: contextRow.ProjectTaskID,
- accountGroupID: contextRow.AccountGroupID,
- qty: hours,
- rate: rate, // if null → pricing defaults
- amount: amount, // if null → formula
- uom: contextRow.UOM,
- description: rule.Description
- );
- }
- }
- private string ResolveBranchCD(int? branchID)
- {
- if (branchID == null) return null;
- Branch br = PXSelect<Branch,
- Where<Branch.branchID, Equal<Required<Branch.branchID>>>>
- .Select(Base, branchID);
- return br?.BranchCD;
- }
- private string ResolveTemplateCD(int? templateProjectID)
- {
- if (templateProjectID == null) return null;
- PMProject tmpl = PMProject.PK.Find(Base, templateProjectID);
- return tmpl?.ContractCD;
- }
- private string ResolveBillRule(PMProject proj)
- {
- return proj?.BillingID;
- }
- private PMRevenueBudget FindRevenueLineByInventoryCd(string inventoryCD)
- {
- if (string.IsNullOrEmpty(inventoryCD))
- return null;
- InventoryItem inv = InventoryItem.UK.Find(Base, inventoryCD);
- if (inv == null) return null;
- int? projID = Base.Project.Current?.ContractID;
- if (projID == null) return null;
- 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;
- 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) or by formula
- 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; // safe default; logic below decides how to set rate
- if (amount == null && qty.HasValue && rate.HasValue && rate.Value != 0m)
- amount = qty * rate;
- var cache = Base.RevenueBudget.Cache;
- var line = new PMRevenueBudget
- {
- ProjectID = projectID,
- Type = AccountType.Income
- };
- line = (PMRevenueBudget)cache.Insert(line);
- 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 (!string.IsNullOrEmpty(description))
- cache.SetValueExt<PMRevenueBudget.description>(line, description);
- // Unit rate + amount logic mirrors the single-code pattern
- if (rate.HasValue && rate.Value != 0m)
- {
- cache.SetValueExt<PMRevenueBudget.curyUnitRate>(line, rate.Value);
- if (amount.HasValue)
- cache.SetValueExt<PMRevenueBudget.curyAmount>(line, amount.Value);
- }
- else
- {
- // No explicit rate: let AR Sales Price default
- cache.SetDefaultExt<PMRevenueBudget.curyUnitRate>(line);
- }
- line = (PMRevenueBudget)cache.Update(line);
- return line;
- }
- private decimal GetRevisedRevenueTotalUI(params string[] excludeCDs)
- {
- 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;
- foreach (PMRevenueBudget line in Base.RevenueBudget.Select().RowCast<PMRevenueBudget>())
- {
- if (line.Type == AccountType.Income && !skip.Contains(line.InventoryID))
- total += line.CuryRevisedAmount ?? 0m;
- }
- return total;
- }
- public static bool IsActive()
- {
- return true;
- }
- }
- }
|