Эх сурвалжийг харах

Used classes to segregate codes out of main logic

Kitt Parker 6 сар өмнө
parent
commit
bbe88f7229

+ 56 - 0
LaborRuleBase.cs

@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ProjectEntryLaborAdjust
+{
+    public abstract class LaborRuleBase
+    {
+        public string InventoryCD { get; }
+        public string Description { get; protected set; }
+
+        public HashSet<string> ExcludedBranchCDs { get; }
+        public HashSet<string> ExcludedTemplateCDs { get; }
+        public HashSet<string> ExcludedBillRules { get; }
+
+        /// <summary>
+        /// Optional fixed unit rate. If null, the AR pricing engine (defaults) should apply.
+        /// </summary>
+        public virtual decimal? FixedUnitRate
+        {
+            get { return null; }
+        }
+
+        protected LaborRuleBase(string inventoryCD, string description = null)
+        {
+            if (string.IsNullOrWhiteSpace(inventoryCD))
+                throw new ArgumentException("inventoryCD");
+
+            InventoryCD = inventoryCD.Trim();
+            Description = string.IsNullOrWhiteSpace(description)
+                ? "Auto-added Labor (" + InventoryCD + ")"
+                : description.Trim();
+
+            ExcludedBranchCDs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+            ExcludedTemplateCDs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+            ExcludedBillRules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+        }
+
+        /// <summary>Compute the quantity (hours) for this labor item from the job size.</summary>
+        public abstract decimal GetHours(decimal jobSize);
+
+        /// <summary>Check if this rule should be skipped under the provided context.</summary>
+        public virtual bool IsExcludedFor(string branchCD, string templateCD, string billRule)
+        {
+            if (!string.IsNullOrWhiteSpace(branchCD) && ExcludedBranchCDs.Contains(branchCD.Trim())) return true;
+            if (!string.IsNullOrWhiteSpace(templateCD) && ExcludedTemplateCDs.Contains(templateCD.Trim())) return true;
+            if (!string.IsNullOrWhiteSpace(billRule) && ExcludedBillRules.Contains(billRule.Trim())) return true;
+            return false;
+        }
+    }
+
+
+
+}

+ 105 - 0
LaborRules.cs

@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ProjectEntryLaborAdjust
+{
+    // ===================== RULE (ALL SETTINGS LIVE HERE) =====================
+    public sealed class L023PmLaborRule : LaborRuleBase
+    {
+        public L023PmLaborRule() : base("L023", "Auto-added PM Labor")
+        {
+            ExcludedBranchCDs.Add("301");
+            ExcludedBranchCDs.Add("112");
+            ExcludedTemplateCDs.Add("ENVIRONMENTAL");
+            ExcludedBillRules.Add("PROGRESS");
+        }
+
+        // Leave null => use AR Sales Prices (no fixed rate)
+        public override decimal? FixedUnitRate
+        {
+            get { return null; }
+        }
+
+        public override decimal GetHours(decimal jobSize)
+        {
+            if (jobSize < 0m) return 0m;
+
+            if (jobSize <= 615m) return 0.5m;
+            if (jobSize <= 1000m) return 1.0m;
+            if (jobSize <= 2500m) return 1.5m;
+            if (jobSize <= 5000m) return 3.0m;
+            if (jobSize <= 7500m) return 3.5m;
+            if (jobSize <= 10000m) return 5.0m;
+            if (jobSize <= 25000m) return 6.0m;
+            if (jobSize <= 50000m) return 7.0m;
+            return 10.5m;
+        }
+    }
+
+    // second rule that DOES supply a fixed rate
+    public sealed class L014LaborRule : LaborRuleBase
+    {
+        public L014LaborRule() : base("L014", "Auto-added Coordinator Labor")
+        {
+            ExcludedBranchCDs.Add("303");
+            ExcludedBranchCDs.Add("112");
+            ExcludedBillRules.Add("PROGRESS");
+            ExcludedTemplateCDs.Add("ENVIRONMENTAL");
+        }
+
+        public override decimal? FixedUnitRate
+        {
+            get { return 40m; } // supply a fixed unit rate
+        }
+
+        public override decimal GetHours(decimal jobSize)
+        {
+            if (jobSize <= 1000m) return 0m;
+            if (jobSize <= 2500m) return .5m;
+            if (jobSize <= 7500m) return 1m;
+            if (jobSize <= 25000m) return 1.5m;
+
+            return 1m;
+        }
+    }
+
+    public sealed class L001LaborRule : LaborRuleBase
+    {
+        public L001LaborRule() : base("L001", "Auto-added Admin Labor")
+        {
+            ExcludedBranchCDs.Add("303");
+            ExcludedBranchCDs.Add("112");
+            ExcludedBillRules.Add("PROGRESS");
+            ExcludedTemplateCDs.Add("ENVIRONMENTAL");
+        }
+
+        public override decimal? FixedUnitRate
+        {
+            get { return 40m; } // supply a fixed unit rate
+        }
+
+        public override decimal GetHours(decimal jobSize)
+        {
+            if (jobSize <= 1000m) return 0m;
+            if (jobSize <= 10000m) return 1m;
+
+            return 3m;
+        }
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+}

+ 156 - 164
ProjectEntryExt.cs

@@ -1,203 +1,216 @@
 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;
 
-//AR202000 use sale prices.  Price Code is the customer. Customer is the project customer.  Qty * Price= Amount
 namespace ProjectEntryLaborAdjust
 {
+  
 
-   
+  
+
+    // ===================== GRAPH EXTENSION (RULE-DRIVEN) =====================
     public class ProjectEntryExt : PXGraphExtension<ProjectEntry>
     {
-        private const string SlotLaborUpdate = "RevBudSetLabor";   // <-- new flag
-        private const string LaborInvCD = "L023"; //"L023";
-        private static readonly HashSet<string> ExcludedBranchCDs = new HashSet<string>{"301"};
-        private static readonly HashSet<string> ExcludedTemplateCDs = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "ENVIRONMENTAL" };
-        private const string ExcludedBillRule = "PROGRESS";
-        private int? _laborInvID;
-        private int? LaborInvID
-        {
-            get
-            {
-                if (_laborInvID == null)
-                    _laborInvID = InventoryItem.UK.Find(Base, LaborInvCD)?.InventoryID;
-                return _laborInvID;
-            }
-        }
+        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;
 
-        protected void _(Events.RowUpdated<PMRevenueBudget> e)
+        public override void Initialize()
         {
-            if (IsExcludedProject())      // ← add this line
-                return;
+            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;
 
-            // 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;
 
+            // Build once
+            string[] laborCDs = _rules.Select(r => r.InventoryCD).ToArray();
+            int?[] laborIDs = laborCDs.Select(GetInventoryIdCached).ToArray();
 
-            // Only react when Original Budgeted Amount changed
-            if (e.Cache.ObjectsEqual<PMRevenueBudget.curyAmount>(row, old))
-            {
+            // If editing any labor line, skip to avoid recursion
+            if (row.InventoryID != null && laborIDs.Contains(row.InventoryID))
                 return;
-            }
-            // Ignore if user is editing the labor line itself
 
-            if (LaborInvID != null && row.InventoryID == LaborInvID) return;
+            // Exclude ALL labor lines from the job-size total
+            decimal totalRevised = GetRevisedRevenueTotalUI(laborCDs);
 
-            // ---- Calculate totals/hours (exclude labor line) -----------------
-            decimal totalRevised = GetRevisedRevenueTotalUI(LaborInvCD);
-            decimal pmHours = GetPMHours(totalRevised);
+            // 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);
 
             PXContext.SetSlot<bool?>(SlotLaborUpdate, true);
             try
             {
-                var cache = Base.RevenueBudget.Cache;
-                PMRevenueBudget laborLine = FindRevenueLineByInventoryCd(LaborInvCD);
-               
-                if (laborLine != null)
+                foreach (var rule in _rules)
                 {
-                    var c = Base.RevenueBudget.Cache;
+                    // Per-rule exclusion check
+                    if (rule.IsExcludedFor(branchCD, templateCD, billRule))
+                        continue;
 
-                    bool qtyChanged = laborLine.Qty != pmHours;
-                    if (qtyChanged)
-                        c.SetValueExt<PMRevenueBudget.qty>(laborLine, pmHours);
-
-                    // Re-run pricing defaults so rate comes from AR prices
-                    if (qtyChanged)
-                        c.SetDefaultExt<PMRevenueBudget.curyUnitRate>(laborLine);
-
-                    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"
-                    );
+                    decimal hours = rule.GetHours(totalRevised);
+                    UpsertLaborLine(rule, hours, row);
                 }
             }
             finally
             {
                 PXContext.SetSlot<bool?>(SlotLaborUpdate, false);
             }
-
-            // ---- Popup & save -----------------------------------
-            /*ShowPopupOnce(totalOrig);*/
-            // Base.Actions.PressSave();
         }
 
+        // Re-assert fixed rates right before save (mirrors the single-code pattern you showed)
         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 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;
 
-        private bool IsExcludedProject()
-        {
-            PMProject proj = Base.Project.Current;
-            if (proj == null) return false;
+                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);
 
-            if (proj.TemplateID != null)
-            {
-                // Any of these 3 options work;
-                // 1) PK.Find (newer builds):
-                PMProject tmpl = PMProject.PK.Find(Base, proj.TemplateID);
-
-                // 2) Fluent BQL (uncomment if you prefer):
-                // PMProject tmpl = SelectFrom<PMProject>
-                //     .Where<PMProject.contractID.IsEqual<@P.AsInt>>.View
-                //     .Select(Base, proj.TemplateID);
-
-                // 3) Classic PXSelectReadonly (also fine):
-                // PMProject tmpl = PXSelectReadonly<PMProject,
-                //     Where<PMProject.contractID, Equal<Required<PMProject.contractID>>>>
-                //     .Select(Base, proj.TemplateID)
-                //     ?.FirstOrDefault()?.GetItem<PMProject>();
-
-                if (tmpl?.ContractCD != null &&
-                    ExcludedTemplateCDs.Contains(tmpl.ContractCD.Trim()))
-                    return true;
+                    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;
 
-            // Branch check (BranchCD = 301) --------------------------------
-            if (proj.DefaultBranchID != null)
-            {
-                Branch br = PXSelect<Branch,
-                                Where<Branch.branchID, Equal<Required<Branch.branchID>>>>
-                            .Select(Base, proj.DefaultBranchID);
+            int? cached;
+            if (_invIdCache.TryGetValue(inventoryCD, out cached))
+                return cached;
 
-                if (br?.BranchCD != null && ExcludedBranchCDs.Contains(br.BranchCD.Trim()))
-                    return true;
-            }
+            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);
 
-            // Billing‑rule check (BillingRule = PROGRESS) ------------------
-            // Field name differs by Acumatica build; common ones shown.
-            string billRule = proj.BillingID;   // e.g., 2021 R1+
+            if (laborLine != null)
+            {
+                bool qtyChanged = laborLine.Qty != hours;
+                if (qtyChanged)
+                    cache.SetValueExt<PMRevenueBudget.qty>(laborLine, hours);
 
-            if (!string.IsNullOrEmpty(billRule) &&
-                string.Equals(billRule.Trim(), ExcludedBillRule,
-                              StringComparison.OrdinalIgnoreCase))
-                return true;
+                // 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);
+                }
 
-            return false;
+                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
+                );
+            }
         }
 
-
-        public static bool IsActive()
+        private string ResolveBranchCD(int? branchID)
         {
-            return true;
+            if (branchID == null) return null;
+            Branch br = PXSelect<Branch,
+                            Where<Branch.branchID, Equal<Required<Branch.branchID>>>>
+                        .Select(Base, branchID);
+            return br?.BranchCD;
         }
 
-
-        protected void _(Events.FieldUpdated<PMRevenueBudget, PMRevenueBudget.curyUnitRate> e)
+        private string ResolveTemplateCD(int? templateProjectID)
         {
-            var r = (PMRevenueBudget)e.Row;
-            if (r == null) return;
-            PXTrace.WriteInformation($"curyUnitRate UPDATED -> {r.CuryUnitRate} (Inv={r.InventoryID})");
+            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;
 
-            // 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
@@ -205,7 +218,6 @@ namespace ProjectEntryLaborAdjust
                                               && 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>>,
@@ -223,13 +235,13 @@ namespace ProjectEntryLaborAdjust
            int? accountGroupID = null, string accountGroupCD = null,
            decimal? qty = null,
            decimal? rate = null,
-           decimal? amount = null,  // if null, amount = qty * rate (when both provided)
+           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 ----------------------------------------------------
+            // Resolve keys
             int? projectID = Base.Project.Current?.ContractID;
             if (projectID == null)
                 throw new PXException();
@@ -248,20 +260,19 @@ namespace ProjectEntryLaborAdjust
                 accountGroupID = PMAccountGroup.UK.Find(Base, accountGroupCD)?.GroupID;
 
             if (qty == null) qty = 0m;
-            if (rate == null) rate = 0m;
-            if (amount == null) amount = qty * rate;
+            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;
 
-            // Insert skeleton row --------------------------------------------
             var cache = Base.RevenueBudget.Cache;
             var line = new PMRevenueBudget
             {
                 ProjectID = projectID,
-                Type = AccountType.Income  // revenue
+                Type = AccountType.Income
             };
 
             line = (PMRevenueBudget)cache.Insert(line);
 
-            // Set fields via SetValueExt so defaults/validations run ----------
             if (taskID != null)
                 cache.SetValueExt<PMRevenueBudget.projectTaskID>(line, taskID);
 
@@ -283,41 +294,26 @@ namespace ProjectEntryLaborAdjust
             if (!string.IsNullOrEmpty(description))
                 cache.SetValueExt<PMRevenueBudget.description>(line, description);
 
-            cache.SetDefaultExt<PMRevenueBudget.curyUnitRate>(line);
+            // 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);
+            }
 
-            // 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 <= 615m: return 0.5m;
-                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 7.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)
@@ -327,22 +323,18 @@ namespace ProjectEntryLaborAdjust
 
             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"
+                    total += line.CuryRevisedAmount ?? 0m;
             }
 
             return total;
         }
 
-
-
-
+        public static bool IsActive()
+        {
+            return true;
+        }
     }
-
-
-
-
 }

+ 2 - 0
ProjectEntryLaborAdjust.csproj

@@ -50,6 +50,8 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="LaborRuleBase.cs" />
+    <Compile Include="LaborRules.cs" />
     <Compile Include="ProjectEntryExt.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>