CRC Validation failing? when writing to a QuoteAsm's UD Fields

Hello!
As part of a large internal project to update our estimating processes, I’m working on an Epicor function that assigns(but does not itself calculate) what we call a “complexity rating” to a given subassembly. This function applies to Quotes, but the calculations are the same between Parts and Jobs. To update any non-0-level Assembly, we just write to the QuoteAsm fields. To update the zero-assembly, we first update mirrored fields on the QuoteDtl line, followed by the corresponding 0-assembly in QuoteAsm. This has worked fine as a BPM in the past (specifically on the Part level) but when I try to write the fields to the QuoteAsm, I get the following error:

/*
Julie Lastname
CompanyName
upd:20250619

SetQuoteLineComplexity()
Updates the Complexity of a Quote's Zero-Assembly

Parameters(int pQuoteNum, int pQuoteLine, int pAssemblySeq)
Returns(bool rSuccess)
*/
rSuccess = false;

var thisAsm = QuoteAsmTS.QuoteAsm.First();
var thisQuoteDtl =  (from   qd in QuoteTS.QuoteDtl 
                    where   qd.QuoteLine == pQuoteLine 
                    select  qd).First();
if (thisAsm != null && thisQuoteDtl != null)
{
  int bufferComplexity = this.ThisLib.CalcQuoteLineComplexity(pQuoteNum, pQuoteLine);
  
  if(bufferComplexity == -1)
  {
    return;
  }
  else if ((bool)thisQuoteDtl["ComplexityCalculated_c"] == true)
  {
    this.PublishInfoMessage
    (
      "Calculated Complexity: " + bufferComplexity + System.Environment.NewLine + 
      "Assembly has not been updated, because Complexity has been marked as Rated." + System.Environment.NewLine + 
      "To use this Complexity Rating, re-run the calculator after unchecking Complexity Rated.", 
      Ice.Common.BusinessObjectMessageType.Information, 
      Ice.Bpm.InfoMessageDisplayMode.Individual, 
      "LibQuoteMisc", "SetQuoteAsmComplexity"
    );
  }
  else if ((bool)thisQuoteDtl["ComplexityOverride_c"] == true)
  {
    
    thisAsm["ComplexityCalculated_c"] = true;
    thisAsm["CalculatedDate_c"] = callContextClient.CurrentUserId;
    thisAsm["CalculatedBy_c"] = BpmFunc.Now();
    //thisAsm.RowMod = "U";
    
    
    thisQuoteDtl["ComplexityCalculated_c"] = true;
    thisQuoteDtl["CalculatedDate_c"] = BpmFunc.Now();
    thisQuoteDtl["CalculatedBy_c"] = callContextClient.CurrentUserId;
    thisQuoteDtl.RowMod = "U";
  }
  else
  {
    thisAsm["ComplexityRating_c"] = bufferComplexity;
    thisAsm["ComplexityCalculated_c"] = true;
    thisAsm["CalculatedDate_c"] = BpmFunc.Now();
    thisAsm["CalculatedBy_c"] = callContextClient.CurrentUserId;

    //update the QuoteAsm 0-sequence row to keep Quote Engineering view in sync
    
    thisAsm["Parameter1"]   = (int)     thisQuoteDtl["Parameter1"];
    thisAsm["Parameter2"]   = (int)     thisQuoteDtl["Parameter2"]; 
    thisAsm["Parameter3"]   = (int)     thisQuoteDtl["Parameter3"]; 
    thisAsm["Parameter4"]   = (int)     thisQuoteDtl["Parameter4"];
    thisAsm["Parameter5"]   = (int)     thisQuoteDtl["Parameter5"]; 
    thisAsm["Parameter6"]   = (int)     thisQuoteDtl["Parameter6"]; 
    thisAsm["Parameter7"]   = (int)     thisQuoteDtl["Parameter7"]; 
    thisAsm["Parameter8"]   = (bool)    thisQuoteDtl["Parameter8"]; 
    thisAsm["Parameter9"]   = (bool)    thisQuoteDtl["Parameter9"]; 
    thisAsm["Parameter10"]  = (bool)    thisQuoteDtl["Parameter10"]; 
    thisAsm["Parameter11"]  = (bool)    thisQuoteDtl["Parameter11"];
    thisAsm["Parameter12"]  = (decimal) thisQuoteDtl["Parameter12"];
    thisAsm["Parameter13"]  = (string)  thisQuoteDtl["Parameter13"];
    thisAsm["Parameter14"]  = (string)  thisQuoteDtl["Parameter14"];
    
    //thisAsm.RowMod = "U";
    
    
    thisQuoteDtl["ComplexityRating_c"] = bufferComplexity;
    thisQuoteDtl["ComplexityCalculated_c"] = true;
    thisQuoteDtl["CalculatedDate_c"] = BpmFunc.Now();
    thisQuoteDtl["CalculatedBy_c"] = callContextClient.CurrentUserId;
    thisQuoteDtl.RowMod = "U";
  }
}

// afterwards, BO.Update()s are called from a widget; if this is successful, we set rSuccess to true;

Basic execution flow is: When the function is run, if this part’s Complexity has already been calculated and locked, simply return the output of the calculator itself; this is useful to test what an overridden complexity value would’ve been without needing to actually unapprove the Complexity rating.
If the Complexity has not been locked in, we assign either the existing Rating (if override is True) or the new calculated rating (if override is False). Both of the paths which write to fields cause this error, identically.

With ‘thisAsm.RowMod = “U”;’ commented out in the two branches it would be in, the code executes successfully and updates the QuoteDtl entry correctly. Unsurprisingly, the sister fields on the QuoteAsm are not taken into account. This isn’t the absolute end of the world if the QuoteDtl fields are set up correctly (other code of mine chooses which record to trust based on the assembly level) but it does mean our Estimators either don’t have that information available, or have incorrect copies of that information.

With RowMod set to U for the QuoteAsm, though, I get the CRC validation error shown above.

QuoteTS contains the standard output of that Quote.GetByID, as does QuoteAsmTS from QuoteAsm.GetByID(Assembly Sequence).

Searching EpiUsers returned some similar errors that other people have had within Parts when there are cyclic references or mismatched Revision numbers, but the parts in question here in a Quote are OTF parts that shouldn’t have a reference outside of the quote itself. I’ve been banging my head into this for a couple of days now, and I’m struggling to understand how setting UD fields through the business objects is causing a cyclic redundancy check to fail, so I figured I’d ask around here.

In the absolute worst case, this all seems to work fine when I do the writes through raw Db updates as opposed to the business objects, but I’m trying to avoid that when at all possible.
That said, nothing seems to be broken doing this; Downstream processes handle the manually-edited QuoteAsm fine.

What am I doing wrong, friends?

I have had issues trying to update two things at once before, have you tried splitting this out so you update one and then the other? Unrelated, these look like they are switched?

thisAsm["CalculatedDate_c"] = callContextClient.CurrentUserId;
thisAsm["CalculatedBy_c"] = BpmFunc.Now();

Well, I never promised I’m not a silly goose…
I’d fixed that in the version running on our Test environment but I hadn’t updated it in the corresponding anonymized version I posted here.

I rewrote this function to loop around a second time, pull the tableset in again, and update the QuoteAsm’s fields. This is still triggering the same error; I wonder if it might be something to do with the validation during QuoteAsm.Update() failing because the tableset doesn’t include the entirity of the quote assembly tree? I’ll try pulling the whole QuoteLine’s assembly tree on Monday and see if that resolves it. If so, good to know an entire tableset is needed for Update()?

Edit: Also realizing this makes a lot more sense here if CRC refers to “circular reference check” as opposed to cyclic redundancy check.