Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,25 +151,26 @@
"steps_include_numbering": "true",
"steps": [
{
"value": "Navigate to the contact list page and use the search function to find the contact.",
"value": "Navigate to the contact list page. **If there is only one contact record displayed, select it directly without using the search function. If there are multiple contacts, use the search function to find the correct contact.**",
"steps_include_numbering": "true",
"steps": [
"{% if page.id == 5052 -%}",
"Use information available to you from the conversation history one by one, starting with the email address, sender's name, company name, phone number, etc.",
"Do not select a contact without performing a search first.",
"If there are multiple contacts, do not select a contact without performing a search first.",
"{% endif -%}"
]
},
{
"value": "If the contact is not found, navigate to the customer list page and use the search function to find the customer.",
"value": "If the contact is not found, navigate to the customer list page. **If there is only one customer record displayed, select it directly without using the search function. If there are multiple customers, use the search function to find the correct customer.**",
"steps_include_numbering": "true",
"steps": [
"{% if page.id == 22 -%}",
"Use information available to you from the conversation history one by one, starting with the email address, sender's name, company name, phone number, etc.",
"Do not select a customer without performing a search first.",
"If there are multiple customers, do not select a customer without performing a search first.",
"{% endif -%}"
]
},
"**Important:** Once you have selected a contact or customer record from the search results, proceed with sales quote creation even if the contact's or customer's name or email address does not exactly match the conversation history. Incoming emails can be mapped to another contact for response routing. The selected record is authoritative; do not request assistance only because of this mismatch.",
"If neither the contact nor the customer is found, then request for assistance."
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;
using System.Agents;

codeunit 4411 "SOA Contact Search Impl"
{
Access = Internal;
EventSubscriberInstance = Manual;
InherentEntitlements = X;
InherentPermissions = X;

var
AgentTaskID: BigInteger;

internal procedure SetAgentTaskID(NewAgentTaskID: BigInteger)
begin
AgentTaskID := NewAgentTaskID;
end;

[EventSubscriber(ObjectType::Page, Page::"Contact List", OnBeforeFindRecord, '', false, false)]
local procedure FindRecordContactFromList(var Rec: Record Contact; Which: Text; var Found: Boolean; var IsHandled: Boolean)
begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Events}$

The new manual subscriber for OnBeforeFindRecord ignores an incoming IsHandled = true, so if another subscriber already handled the Contact List search, this code can still rewrite Rec and Found and change the final result. Add an early if IsHandled then exit; guard so the Sales Order Agent override cooperates with other subscribers on the new base-app event.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

    begin
        if IsHandled then
            exit;

        FindRecordContact(Rec, Which, Found, IsHandled);
    end;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

FindRecordContact(Rec, Which, Found, IsHandled);
end;

local procedure FindRecordContact(var Rec: Record Contact; Which: Text; var Found: Boolean; var IsHandled: Boolean)
var
AgentTaskMessage: Record "Agent Task Message";
SOATaskContactOverride: Record "SOA Task Contact Override";
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
OriginalFilterGroup: Integer;
begin
if AgentTaskID = 0 then
exit;

AgentTaskMessage.SetLoadFields(ID);
AgentTaskMessage.SetRange("Task ID", AgentTaskID);
AgentTaskMessage.SetRange(Type, AgentTaskMessage.Type::Input);
AgentTaskMessage.SetFilter(Status, '<>%1&<>%2', AgentTaskMessage.Status::Discarded, AgentTaskMessage.Status::Rejected);
AgentTaskMessage.SetCurrentKey("Task ID", SystemCreatedAt);
AgentTaskMessage.Ascending(false);
if not AgentTaskMessage.FindFirst() then
exit;

if not SOATaskContactOverride.Get(AgentTaskID, AgentTaskMessage.ID) then
exit;
if not SOAFiltersImpl.IsContactOverrideTrusted(SOATaskContactOverride) then
exit;

OriginalFilterGroup := Rec.FilterGroup();
Rec.FilterGroup(11);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filter is ANDed with the security filter (group 10) and with the agent search filter in the user filter group. If the agent searches by the sender name or email, which is exactly the failing scenario, the result is empty and the mapped contact is invisible. The fix then works only because the prompt now tells the agent not to search, which is not a guarantee. The group 11 filter is also never cleared when the current message has no valid override. Please clear group 11 on every call and neutralize the conflicting user filter when an override is pinned.

Rec.SetRange("No.", SOATaskContactOverride."Contact No.");
Rec.FilterGroup(OriginalFilterGroup);
Found := Rec.Find(Which);
IsHandled := true;
end;
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,19 +412,14 @@ page 4404 "SOA Email Message"
else
TaskMessageID := Rec.ID;

if SOATaskContactOverride.Get(Rec."Task ID", TaskMessageID) then
if SOATaskContactOverride.Get(Rec."Task ID", TaskMessageID) and SOAFiltersImpl.IsContactOverrideTrusted(SOATaskContactOverride) then
if SOATaskContactOverride."Contact No." <> '' then
if Contact.Get(SOATaskContactOverride."Contact No.") then begin
ContactCount := 1;
exit(true);
end;

Contact.SetFilter("E-Mail", SOAFiltersImpl.GetSafeFromEmailFilter(EmailAddress));
ContactCount := Contact.Count();
if not Contact.FindFirst() then
exit(false);

exit(true);
exit(SOAFiltersImpl.FindContactByEmail(Contact, EmailAddress, ContactCount));
end;

local procedure GetSOAEmail(var AgentTaskMessage: Record "Agent Task Message"): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,9 @@ codeunit 4418 "SOA Reply Retry Mgt."
end;

local procedure ValidateMessageAccess(AgentTaskMessage: Record "Agent Task Message"; var SOASetup: Record "SOA Setup")
var
OwnerUserSecurityID: Guid;
begin
SOASetup.GetBasedOnAgentUserSecurityID(AgentTaskMessage."Agent User Security ID", true);
OwnerUserSecurityID := SOASetup."Owner User Security ID";
if IsNullGuid(OwnerUserSecurityID) then
OwnerUserSecurityID := SOASetup."User Security ID";

if (UserSecurityId() <> OwnerUserSecurityID) and (UserSecurityId() <> SOASetup."User Security ID") then
if not SOASetup.IsAuthorizedUserSecurityID(UserSecurityId()) then
Error(ReplyNotAuthorizedErr);
end;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#pragma warning disable AS0007
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;
using System.Agents;
using System.Email;
using System.Telemetry;
Expand All @@ -24,7 +25,11 @@ codeunit 4419 "SOA Send Reply"
AgentMessage: Codeunit "Agent Message";
Email: Codeunit Email;
EmailMessage: Codeunit "Email Message";
CCRecipients: List of [Text];
EmptyBCCRecipients: List of [Text];
ToRecipients: List of [Text];
Body: Text;
MappedContactEmail: Text;
Subject: Text;
begin
Rec.Get(Rec."Task ID", Rec.ID);
Expand All @@ -36,11 +41,24 @@ codeunit 4419 "SOA Send Reply"

Subject := StrSubstNo(EmailSubjectTxt, InputAgentTaskMessage."Task ID");
Body := AgentMessage.GetText(Rec);
EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID");
MappedContactEmail := GetMappedContactEmail(InputAgentTaskMessage);

if MappedContactEmail <> '' then begin
ValidateMessageAccess(Rec, SOASetup);
ToRecipients.Add(MappedContactEmail);
GetOriginEmailCCRecipients(InputAgentTaskMessage, CCRecipients);
EmailMessage.CreateReply(ToRecipients, Subject, Body, true, InputAgentTaskMessage."External ID", CCRecipients, EmptyBCCRecipients);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateReplyAll addressed the sender plus all original To and CC recipients. This path sends only to the mapped contact plus CC, so any other To participant in the thread is silently dropped. EmptyBCCRecipients is also always empty, although the PR description says BCC is preserved. Please rebuild the original recipient list and replace only the sender address, keeping the other To recipients.

end else
EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID");

AddMessageAttachments(EmailMessage, Rec);

if not Email.ReplyAll(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);
if MappedContactEmail <> '' then begin
if not Email.Reply(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);
end else
if not Email.ReplyAll(EmailMessage, SOASetup."Email Account ID", SOASetup."Email Connector") then
Error(EmailReplyFailedErr);

AgentMessage.SetStatusToSent(Rec."Task ID", Rec.ID);
end;
Expand All @@ -53,6 +71,96 @@ codeunit 4419 "SOA Send Reply"
EmailSubjectTxt: Label 'Sales order agent reply to task %1', Comment = '%1 = Agent Task id';
EmailReplyFailedErr: Label 'The email reply could not be sent.';
InvalidReplyMessageErr: Label 'Only reviewed output messages can be sent as replies.';
ReplyNotAuthorizedErr: Label 'You are not authorized to send this reply.';
InvalidMappedContactErr: Label 'The contact mapping for this message is no longer valid. Choose another contact before sending the reply.';
MappedContactEmailMissingErr: Label 'The mapped contact %1 does not have a primary email address. Add an email address to the contact or choose another contact before sending the reply.', Comment = '%1 = Contact No.';
MultipleAlternateEmailMappingsErr: Label 'The sender''s alternate email address is assigned to more than one contact. Remove the duplicate alternate email mappings before sending the reply.';
MappedContactErrorTitleErr: Label 'Contact mapping requires attention';
MappedContactErrorDetailedMessageErr: Label 'Open the source email message and correct its contact mapping or the mapped contact''s primary email address, then retry the reply.';
ShowSourceEmailMessageLbl: Label 'Show source email message';
OriginEmailUnavailableErr: Label 'The original email could not be opened, so the mapped-contact reply was not sent.';

local procedure GetMappedContactEmail(InputAgentTaskMessage: Record "Agent Task Message"): Text
var
SOATaskContactOverride: Record "SOA Task Contact Override";
Contact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
Comment thread
tomasevicst marked this conversation as resolved.
ContactCount: Integer;
begin
if SOATaskContactOverride.Get(InputAgentTaskMessage."Task ID", InputAgentTaskMessage.ID) then begin
Comment thread
tomasevicst marked this conversation as resolved.
if not SOAFiltersImpl.IsContactOverrideTrusted(SOATaskContactOverride) then
ErrorMappedContact(InvalidMappedContactErr, InputAgentTaskMessage);
if SOATaskContactOverride."Contact No." = '' then
ErrorMappedContact(InvalidMappedContactErr, InputAgentTaskMessage);

Contact.SetLoadFields("E-Mail");
if not Contact.Get(SOATaskContactOverride."Contact No.") then
ErrorMappedContact(InvalidMappedContactErr, InputAgentTaskMessage);
if Contact."E-Mail" = '' then
ErrorMappedContact(StrSubstNo(MappedContactEmailMissingErr, Contact."No."), InputAgentTaskMessage);

exit(Contact."E-Mail");
end;

// Only the alternate email represents a persistent mapping; primary email matches keep the existing Reply All behavior.
Comment thread
tomasevicst marked this conversation as resolved.
if SOAFiltersImpl.FindContactByAlternateEmail(Contact, InputAgentTaskMessage.From, ContactCount) then begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindContactByAlternateEmail looks only at E-Mail 2. A unique E-Mail 2 match is accepted even when a different contact holds the same address in the primary E-Mail field, so the reply can be routed to the wrong contact. Please check uniqueness across both email fields and fall back to Reply All when they resolve to different contacts.

if ContactCount > 1 then
ErrorMappedContact(MultipleAlternateEmailMappingsErr, InputAgentTaskMessage);
if Contact."E-Mail" = '' then
ErrorMappedContact(StrSubstNo(MappedContactEmailMissingErr, Contact."No."), InputAgentTaskMessage);

exit(Contact."E-Mail");
end;

exit('');
end;

/// <summary>
/// Ensures that a mapped reply belongs to the selected SOA setup and is sent by its configured owner or agent.
/// Mapped replies redirect the original thread, so this check is enforced independently of the codeunit's internal access.
/// </summary>
local procedure ValidateMessageAccess(AgentTaskMessage: Record "Agent Task Message"; SOASetup: Record "SOA Setup")
begin
if AgentTaskMessage."Agent User Security ID" <> SOASetup."User Security ID" then
Error(ReplyNotAuthorizedErr);
if not SOASetup.IsAuthorizedUserSecurityID(UserSecurityId()) then
Error(ReplyNotAuthorizedErr);
end;

local procedure ErrorMappedContact(ErrorMessage: Text; InputAgentTaskMessage: Record "Agent Task Message")
Comment thread
tomasevicst marked this conversation as resolved.
var
MappedContactErrorInfo: ErrorInfo;
begin
MappedContactErrorInfo.Title := MappedContactErrorTitleErr;
MappedContactErrorInfo.Message := ErrorMessage;
MappedContactErrorInfo.DetailedMessage := MappedContactErrorDetailedMessageErr;
MappedContactErrorInfo.PageNo := Page::"SOA Email Message";
MappedContactErrorInfo.RecordId := InputAgentTaskMessage.RecordId();
MappedContactErrorInfo.AddNavigationAction(ShowSourceEmailMessageLbl);
Error(MappedContactErrorInfo);
end;

local procedure GetOriginEmailCCRecipients(InputAgentTaskMessage: Record "Agent Task Message"; var CCRecipients: List of [Text])
Comment thread
tomasevicst marked this conversation as resolved.
var
SOAEmail: Record "SOA Email";
EmailInbox: Record "Email Inbox";
OriginEmailMessage: Codeunit "Email Message";
begin
SOAEmail.SetLoadFields("Email Inbox ID");
Comment thread
tomasevicst marked this conversation as resolved.
SOAEmail.SetRange("Task ID", InputAgentTaskMessage."Task ID");
SOAEmail.SetRange("Task Message ID", InputAgentTaskMessage.ID);
if not SOAEmail.FindFirst() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Error\ Handling}$

The mapped-contact reply path falls back to a plain Error when the original email, inbox row, or stored message cannot be reopened (GetOriginEmailCCRecipients). This is the same recoverable contact-mapping flow that already uses ErrorInfo with a navigation action (ErrorMappedContact) elsewhere in this codeunit, so these failures should also raise a navigable ErrorInfo instead of a dead-end plain Error.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        if not SOAEmail.FindFirst() then
            ErrorMappedContact(OriginEmailUnavailableErr, InputAgentTaskMessage);

        EmailInbox.SetLoadFields("Message Id");
        if not EmailInbox.Get(SOAEmail."Email Inbox ID") then
            ErrorMappedContact(OriginEmailUnavailableErr, InputAgentTaskMessage);

        if not OriginEmailMessage.Get(EmailInbox."Message Id") then
            ErrorMappedContact(OriginEmailUnavailableErr, InputAgentTaskMessage);

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Error(OriginEmailUnavailableErr);

EmailInbox.SetLoadFields("Message Id");
if not EmailInbox.Get(SOAEmail."Email Inbox ID") then
Error(OriginEmailUnavailableErr);

if not OriginEmailMessage.Get(EmailInbox."Message Id") then
Error(OriginEmailUnavailableErr);

OriginEmailMessage.GetRecipients(Enum::"Email Recipient Type"::Cc, CCRecipients);
end;

local procedure AddMessageAttachments(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessage: Record "Agent Task Message")
var
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ codeunit 4398 "SOA Task Message"
SentAgentTaskMessage: Record "Agent Task Message";
SOATaskContactOverride: Record "SOA Task Contact Override";
OverrideContact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
ContactCount: Integer;
begin
Clear(ToAddress);
if OutputAgentTaskMessage.Type <> OutputAgentTaskMessage.Type::Output then
Expand All @@ -100,7 +102,7 @@ codeunit 4398 "SOA Task Message"
if SentAgentTaskMessage.From = '' then
exit(false);

if SOATaskContactOverride.Get(OutputAgentTaskMessage."Task ID", OutputAgentTaskMessage."Input Message ID") then
if SOATaskContactOverride.Get(OutputAgentTaskMessage."Task ID", OutputAgentTaskMessage."Input Message ID") and SOAFiltersImpl.IsContactOverrideTrusted(SOATaskContactOverride) then
if SOATaskContactOverride."Contact No." <> '' then begin
OverrideContact.SetLoadFields("E-Mail");
if OverrideContact.Get(SOATaskContactOverride."Contact No.") then
Expand All @@ -110,29 +112,31 @@ codeunit 4398 "SOA Task Message"
end;
end;

if SOAFiltersImpl.FindContactByEmail(OverrideContact, SentAgentTaskMessage.From, ContactCount) and (ContactCount = 1) then
if OverrideContact."E-Mail" <> '' then begin
ToAddress := OverrideContact."E-Mail";
exit(true);
end;

ToAddress := SentAgentTaskMessage.From;
exit(true);
end;

internal procedure MessageRequiresReview(SOASetup: Record "SOA Setup"; EmailInbox: Record "Email Inbox"; IsFirstMessageInTask: Boolean): Boolean
var
Contact: Record Contact;
SOAFiltersImpl: Codeunit "SOA Filters Impl.";
SOAInputMessageReview: Enum "SOA Input Message Review";
begin
// If we have the same review setting for both registered and unregistered senders,
// then we can skip trying to find the contact.
if SOASetup."Known Sender In. Msg. Review" = SOASetup."Unknown Sender In. Msg. Review" then
SOAInputMessageReview := SOASetup."Known Sender In. Msg. Review"
else begin
else
// Check if the sender is a registered contact
Contact.SetFilter("E-Mail", SOAFiltersImpl.GetSafeFromEmailFilter(EmailInbox."Sender Address"));
Contact.ReadIsolation := IsolationLevel::ReadCommitted;
if Contact.IsEmpty() then
if not SOAFiltersImpl.ContactExistsByEmail(EmailInbox."Sender Address") then
SOAInputMessageReview := SOASetup."Unknown Sender In. Msg. Review"
else
SOAInputMessageReview := SOASetup."Known Sender In. Msg. Review";
end;

case SOAInputMessageReview of
SOAInputMessageReview::"All Messages":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Agent.SalesOrderAgent;

using Microsoft.CRM.Contact;

pageextension 4411 "SOA Contact List Ext" extends "Contact List"
{
layout
{
addafter("E-Mail")
{
field("SOA E-Mail 2"; Rec."E-Mail 2")
{
ApplicationArea = Basic, Suite;
Caption = 'Email 2';
ToolTip = 'Specifies an alternative email address for the contact.';
Visible = IsAgentSession;
}
Comment on lines +15 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

The new Contact List field exposes Rec."E-Mail 2" as a normal editable field in agent sessions. Any user who already has Contact-modify permission can change this persistent sender mapping directly from the list, bypassing the ValidateContactMappingAccess() check that the codeunit path (SelectContactAndUpdateEmail) enforces, even though "E-Mail 2" now drives known-sender classification and mapped-contact reply routing. Make the list field read-only and keep alternate-email updates behind the validated SelectContactAndUpdateEmail(...) flow, or enforce the same authorization on direct edits.

Suggested change
field("SOA E-Mail 2"; Rec."E-Mail 2")
{
ApplicationArea = Basic, Suite;
Caption = 'Email 2';
ToolTip = 'Specifies an alternative email address for the contact.';
Visible = IsAgentSession;
}
field("SOA E-Mail 2"; Rec."E-Mail 2")
{
ApplicationArea = Basic, Suite;
Caption = 'Email 2';
ToolTip = 'Specifies an alternative email address for the contact.';
Editable = false;
Visible = IsAgentSession;
}

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

}
}

trigger OnOpenPage()
var
SOAKPITrackAll: Codeunit "SOA - KPI Track All";
AgentTaskID: BigInteger;
begin
IsAgentSession := SOAKPITrackAll.IsOrderTakerAgentSession(AgentTaskID);
end;

var
IsAgentSession: Boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ permissionset 4406 "SOA - Objects"
tabledata "Contact" = R,
tabledata "SOA Email" = RIM,
tabledata "SOA Reply Attempt" = rimd,
tabledata "SOA Task Contact Override" = RIM,
tabledata "SOA Task Contact Override" = R,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Security}$

The permission-set change from RIM to R on tabledata "SOA Task Contact Override" is still a direct grant. Because SOAFiltersImpl already performs the controlled insert/modify path with its own direct RIM access, callers do not need direct table reads via this permission set for SelectContactAndSetOverride to keep working. Leaving direct R here means any assignee of this permission set can still read the mapping table through ad-hoc runtime surfaces instead of only through the owning code, undermining the intent of tightening access.

Suggested change
tabledata "SOA Task Contact Override" = R,
tabledata "SOA Task Contact Override" = r,

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

page "Contact Card" = X,
page "Contact List" = X,
page "Customer Card" = X,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pagecustomization "SOA Contact List" customizes "Contact List"
{
Visible = true;
}
modify("SOA E-Mail 2")
{
Visible = true;
}
modify("Fax No.")
{
Visible = true;
Expand Down
Loading
Loading