|
|
@@ -1,341 +1,342 @@
|
|
|
-using System;
|
|
|
-using System.Collections.Generic;
|
|
|
-using System.Linq;
|
|
|
-using PX.Common;
|
|
|
-using PX.Data;
|
|
|
-using PX.Objects.GL;
|
|
|
-using PX.Objects.IN;
|
|
|
-using PX.Objects.PM;
|
|
|
-
|
|
|
-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);
|
|
|
- UpsertLaborLine(rule, hours, row);
|
|
|
- }
|
|
|
- }
|
|
|
- finally
|
|
|
- {
|
|
|
- PXContext.SetSlot<bool?>(SlotLaborUpdate, false);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Re-assert fixed rates right before save (mirrors the single-code pattern you showed)
|
|
|
- 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;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+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 (mirrors the single-code pattern you showed)
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+}
|