0
votes

In the customer screen, I've got a few Attributes configured as per Customer Class

enter image description here

I'd like these attributes to be copied into Sales Order when I raised for this Customer. Sample screen as below :

enter image description here

The sales order attributes grid will be read only. I know how to add a tab item and a grid. But I'm not sure how to configure the "attributes" attribute field for the Sales order. I'm assuming I can piggy back on the Customer's "attribute" definition ?

Which I just did :

public class SOOrderExt : PXCacheExtension<PX.Objects.SO.SOOrder>
{
  #region Attributes

[CRAttributesField(typeof (Customer.customerClassID))]
public virtual string[] Attributes { get; set; }
  #endregion
}

And the Answer view :

namespace PX.Objects.SO
{
  public class SOOrderEntry_Extension:PXGraphExtension<SOOrderEntry>
  {
    [PXViewName("Answers")]
    public CRAttributeList<Customer>Answers;
  }
}

It displays the Customer's Attributes...great ! But they need to be saved against the Sales Order. If down the line, the Customer's attributes have changed. The Sales Order should have the copy the original Customer's attributes when the Order was first raised. So how do I do that ? Thanks !

1

1 Answers

2
votes

The problem is the reference for the attribute is being linked to the customer and not the order like you need it to (to save the attributes). To do this we need to write our own query/association call within the CRAttributeList class. I created the following inheriting class and was able to get the Attributes to stick to the order by associating the order ref noteid to the saved answers. The CRAttributeList class doesn't override nicely so there is a lot of copied code. You can browse the source code to see the full class and update anything as needed. I assume this can be simplified but for now its a working answer.

Keep your order dac extension the way you have it...

public class SOOrderExt : PXCacheExtension<PX.Objects.SO.SOOrder>
{
    [CRAttributesField(typeof (Customer.customerClassID))]
    public virtual string[] Attributes { get; set; }
}

Replace the view to use the new class...

[PXViewName("Answers")]
public SalesCustomerAttributeList Answers;

New class... (changing SelectDelegate & base SelectInternal)

public class SalesCustomerAttributeList : CRAttributeList<Customer>
{
    public SalesCustomerAttributeList(PXGraph graph) : base(graph)
    {
    }

    //Copy of private method from CRAttributeList
    protected string GetClassId(object row)
    {
        var classIdField = GetClassIdField(row);
        if (classIdField == null)
            return null;

        var entityCache = _Graph.Caches[row.GetType()];

        var classIdValue = entityCache.GetValueExt(row, classIdField.Name);

        return classIdValue?.ToString()?.Trim();
    }

    //Copy of private method from CRAttributeList
    protected Type GetClassIdField(object row)
    {
        if (row == null)
            return null;


        var fieldAttribute =
            _Graph.Caches[row.GetType()].GetAttributes(row, null)
                .OfType<CRAttributesFieldAttribute>()
                .FirstOrDefault();

        if (fieldAttribute == null)
            return null;

        return fieldAttribute.ClassIdField;
    }

    //Copy of private method from CRAttributeList
    protected Type GetEntityTypeFromAttribute(object row)
    {
        var classIdField = GetClassIdField(row);
        if (classIdField == null)
            return null;

        return classIdField.DeclaringType;
    }

    //Override to use desired query for sales order and customer/customer class related attributes
    protected override IEnumerable SelectDelegate()
    {
        return this.SelectInternal(
            (Customer)_Graph.Caches<Customer>()?.Current,
            (SOOrder)_Graph.Caches<SOOrder>()?.Current);
    }

    /// <summary>
    /// Find the customer default value based on the given answer
    /// </summary>
    protected bool TryGetCustomerAttributeValue(CSAnswers classAnswer, List<CSAnswers> customerAnswers, out string customerDefault)
    {
        customerDefault = null;
        if (classAnswer == null || customerAnswers == null)
        {
            return false;
        }

        foreach (var customerAttribute in customerAnswers)
        {
            if (customerAttribute.AttributeID != classAnswer.AttributeID)
            {
                continue;
            }
            customerDefault = customerAttribute.Value;
            return true;
        }

        return false;
    }

    protected List<CSAnswers> GetCustomerAttributes(Customer customerRow)
    {
        return PXSelect<CSAnswers, Where<CSAnswers.refNoteID, Equal<Required<CSAnswers.refNoteID>>>>
                .Select(_Graph, customerRow.NoteID).FirstTableItems.ToList();
    }

    //Override to use desired query for sales order and customer/customer class related attributes
    protected IEnumerable<CSAnswers> SelectInternal(Customer customerRow, SOOrder orderRow)
    {
        if (orderRow == null || customerRow == null)
        {
            yield break;
        }

        var noteId = GetNoteId(orderRow);

        if (!noteId.HasValue)
            yield break;

        var answerCache = _Graph.Caches[typeof(CSAnswers)];
        var orderCache = _Graph.Caches[orderRow.GetType()];

        List<CSAnswers> answerList;

        var status = orderCache.GetStatus(orderRow);

        if (status == PXEntryStatus.Inserted || status == PXEntryStatus.InsertedDeleted)
        {
            answerList = answerCache.Inserted.Cast<CSAnswers>().Where(x => x.RefNoteID == noteId).ToList();
        }
        else
        {
            answerList = PXSelect<CSAnswers, Where<CSAnswers.refNoteID, Equal<Required<CSAnswers.refNoteID>>>>
                .Select(_Graph, noteId).FirstTableItems.ToList();
        }

        var classId = GetClassId(customerRow);

        CRAttribute.ClassAttributeList classAttributeList = new CRAttribute.ClassAttributeList();
        if (classId != null)
        {
            classAttributeList = CRAttribute.EntityAttributes(GetEntityTypeFromAttribute(customerRow), classId);
        }

        //when coming from Import scenarios there might be attributes which don't belong to entity's current attribute class or the entity might not have any attribute class at all
        if (_Graph.IsImport && PXView.SortColumns.Any() && PXView.Searches.Any())
        {
            var columnIndex = Array.FindIndex(PXView.SortColumns,
                x => x.Equals(typeof(CSAnswers.attributeID).Name, StringComparison.OrdinalIgnoreCase));

            if (columnIndex >= 0 && columnIndex < PXView.Searches.Length)
            {
                var searchValue = PXView.Searches[columnIndex];

                if (searchValue != null)
                {
                    //searchValue can be either AttributeId or Description
                    var attributeDefinition = CRAttribute.Attributes[searchValue.ToString()] ??
                                            CRAttribute.AttributesByDescr[searchValue.ToString()];

                    if (attributeDefinition == null)
                    {
                        throw new PXSetPropertyException(PX.Objects.CR.Messages.AttributeNotValid);
                    }
                    //avoid duplicates

                    if (classAttributeList[attributeDefinition.ToString()] == null)
                    {
                        classAttributeList.Add(new CRAttribute.AttributeExt(attributeDefinition, null, false, true));
                    }
                }
            }
        }

        if (answerList.Count == 0 && classAttributeList.Count == 0)
        {
            yield break;
        }

        //attribute identifiers that are contained in CSAnswers cache/table but not in class attribute list
        List<string> attributeIdListAnswers =
            answerList.Select(x => x.AttributeID)
                .Except(classAttributeList.Select(x => x.ID))
                .Distinct()
                .ToList();

        //attribute identifiers that are contained in class attribute list but not in CSAnswers cache/table
        List<string> attributeIdListClass =
            classAttributeList.Select(x => x.ID)
                .Except(answerList.Select(x => x.AttributeID))
                .ToList();

        //attribute identifiers which belong to both lists
        List<string> attributeIdListIntersection =
            classAttributeList.Select(x => x.ID)
                .Intersect(answerList.Select(x => x.AttributeID))
                .Distinct()
                .ToList();


        var cacheIsDirty = answerCache.IsDirty;

        List<CSAnswers> output = new List<CSAnswers>();

        //attributes contained only in CSAnswers cache/table should be added "as is"
        output.AddRange(answerList.Where(x => attributeIdListAnswers.Contains(x.AttributeID)));

        var customerAnswers = GetCustomerAttributes(customerRow);

        //attributes contained only in class attribute list should be created and initialized with default value
        foreach (var attributeId in attributeIdListClass)
        {
            var classAttributeDefinition = classAttributeList[attributeId];

            if (PXSiteMap.IsPortal && classAttributeDefinition.IsInternal)
                continue;

            if (!classAttributeDefinition.IsActive)
                continue;

            CSAnswers answer = (CSAnswers)answerCache.CreateInstance();
            answer.AttributeID = classAttributeDefinition.ID;
            answer.RefNoteID = noteId;
            answer.Value = GetDefaultAnswerValue(classAttributeDefinition);
            if (TryGetCustomerAttributeValue(answer, customerAnswers, out var customerValue))
            {
                answer.Value = customerValue;
            }

            if (classAttributeDefinition.ControlType == CSAttribute.CheckBox)
            {
                bool value;
                if (bool.TryParse(answer.Value, out value))
                    answer.Value = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture);
                else if (answer.Value == null)
                    answer.Value = 0.ToString();
            }

            answer.IsRequired = classAttributeDefinition.Required;
            answer = (CSAnswers)(answerCache.Insert(answer) ?? answerCache.Locate(answer));
            output.Add(answer);
        }

        //attributes belonging to both lists should be selected from CSAnswers cache/table with and additional IsRequired check against class definition
        foreach (CSAnswers answer in answerList.Where(x => attributeIdListIntersection.Contains(x.AttributeID)).ToList())
        {
            var classAttributeDefinition = classAttributeList[answer.AttributeID];

            if (PXSiteMap.IsPortal && classAttributeDefinition.IsInternal)
                continue;

            if (!classAttributeDefinition.IsActive)
                continue;

            if (answer.Value == null && classAttributeDefinition.ControlType == CSAttribute.CheckBox)
                answer.Value = bool.FalseString;

            if (answer.IsRequired == null || classAttributeDefinition.Required != answer.IsRequired)
            {
                answer.IsRequired = classAttributeDefinition.Required;

                var fieldState = View.Cache.GetValueExt<CSAnswers.isRequired>(answer) as PXFieldState;
                var fieldValue = fieldState != null && ((bool?)fieldState.Value).GetValueOrDefault();

                answer.IsRequired = classAttributeDefinition.Required || fieldValue;
            }

            output.Add(answer);
        }

        answerCache.IsDirty = cacheIsDirty;

        output =
            output.OrderBy(
                x =>
                    classAttributeList.Contains(x.AttributeID)
                        ? classAttributeList.IndexOf(x.AttributeID)
                        : (x.Order ?? 0))
                .ThenBy(x => x.AttributeID)
                .ToList();

        short attributeOrder = 0;

        foreach (CSAnswers answer in output)
        {
            answer.Order = attributeOrder++;
            yield return answer;
        }
    }
}

For examples on the page entry for the attributes tab you can look at the Customer page - attributes tab. I copied this tab for my testing of this answer.