Adding the Payment Advice to AX 2012 Print Management

by | May 5, 2014 | Development, Dynamics AX

In AX 2012 Print Management, the Payment Advice is not a supported document type out of the box. We recently had a customer request to add this feature so that their vendors could receive the Payment Advice via email. This blog describes the changes needed to meet this requirement. Adding the Payment Advice to AX 2012 Print Management is only one part of the solution. Because the Payment Advice report is designed to print all vendors at once, rather than running the report for each vendor separately, modifications must be made to ensure that each vendor receives a separate pdf file with only his own Payment Advice.

It is not the intention of this blog to rehash the detailed information in the AX 2012 Print Management Integration Guide. Instead, I wanted to expand on the whitepaper with the specific code needed for the Payment Advice. With this in mind, I will not go into a detailed explanation of every class and method, but will only highlight a few important details.

First, it may be helpful to envision the hierarchy as you work through this code.

For the payment advice, we want users to be able to set the preferences at the document level and at the vendor level, so the hierarchy looks like this…

Purch (accounts payable node)

VendTable (vendor node)

BankPaymAdvice (new doc node)

The following existing AX objects were modified for this feature:

Enums:

PrintMgmtNodeType – Added BankPaymAdvice

PrintMgmtDocumentType – Added BankPaymAdvice

Tables:

BankPaymAdviceVendTemp – Added field VendAccount, Added Relation VendTable

PrintMgmtReportFormat – Methods\populate – modified to support the new doc type

Classes:

PrintMgmtNode – Methods\construct – modified for the new node type

PrintMgmtNode_Purch – Methods\getDocumentTypes – modified for the new doc type

PrintMgmtNode_VendTable – Methods\getDocumentTypes – modified for the new doc type

PrintMgmtHierarchy_Purch:

Methods\getNodesImplementation – add BankPaymAdvice as a supported node

Methods\getParentImplementation – add support for BankPaymAdvice (see Objects of Interest)

PrintMgmtDocType:

Methods\getDefaultReportFormat – return the report name for BankPaymAdvice

Methods\getQueryTableId– modified to add the BankPaymAdviceVendTmp table for BankPaymAdvice

VendOutPaym_NACHA – Add the Use Print Management field to the dialog and pass along to the controller class

Methods modified:

ClassDeclaration

Dialog

getFromDialog

init

printPaymAdvice

unpack

 

The follow custom objects were created for this feature:

Classes:

PrintMgmtNode_BankPaymAdvice – supporting class for the new node type

FDBankPaymAdviceVendController – controller class for the report. Extends SrsPrintMgmtController.

All of the code changes are included below:

Table: PrintMgmtReportFormat

Method\Populate: added the BankPaymAdvice doc type

addAX(PrintMgmtDocumentType::PurchRFQ);

addAX(PrintMgmtDocumentType::PurchRFQAccept);

addAX(PrintMgmtDocumentType::PurchRFQReject);

addAX(PrintMgmtDocumentType::PurchRFQReturn);

addAX(PrintMgmtDocumentType::PurchaseOrderConfirmationRequest);

//Stoneridge Software Begin

addAX(PrintMgmtDocumentType::BankPaymAdvice);

//Stoneridge Software End

 New Classes:

The out of box report uses a controller class that extends SRSReportRunController. This new class is basically a modified copy of BankPaymAdviceVendController extending SrsPrintMgmtController. The entire class is included here.

public class FDBankPaymAdviceVendController extends SrsPrintMgmtController

{

VendOutPaym                 vendOutPaym;

List                       vendOutPaymList;

str                         paymRef;

VendTrans                  vendTransOriginalInvoice;

BankPaymAdviceVendTmp       bankPaymAdviceVendTmp;

CompanyInfo                 companyInfo;

VendTable                   vendTable;

LedgerJournalTrans         ledgerJournalTrans;

SpecTrans                  specTrans;

VendTransOpen               vendTransOpen;

VendTrans                   vendTrans;

Common                     transtable;

UsePrintMgmt               paymAdvUsePrintMgmt;

str                         newPaymRefLable;

Amount                     settledAmount;

CurrencyCode               settledCurrencyCode;

str                         groupBy;

int                         currentSessionId;

PrintCopyOriginal                           printCopyOriginal;

LogisticsAddressing         companyAddress;

#define.ReportName(‘BankPaymAdviceVend.Report’)

}

private str currentSessionId()

{

return int2str(currentSessionId);

}

private VendCashDiscAmount getCashDiscAmount()

{

// gets the cash discount that is specified when settling an invoice.

VendTransCashDisc   vendTransCashDisc;

changecompany(vendTransOpen.company())

{

vendTransCashDisc = VendTransCashDisc::findByUseCashDisc(vendTransOpen.TableId,

vendTransOpen.RecId,

vendTransOpen.UseCashDisc,

ledgerJournalTrans.TransDate);

}

 

return -vendTransCashDisc.CashDiscAmount;

}

protected void initPrintMgmtReportRun()

{

paymAdvUsePrintMgmt = this.parmArgs().parmEnum();

printMgmtReportRun=PrintMgmtReportRun::construct(

PrintMgmtHierarchyType::Purch,PrintMgmtNodeType::BankPaymAdvice,PrintMgmtDocumentType::BankPaymAdvice);

printMgmtReportRun.parmForcePrintJobSettings(!paymAdvUsePrintMgmt);

printMgmtReportRun.parmReportRunController(this);

}

private void insertBankPaymAdviceVendTmp(boolean isSettledVoucherAmount,

boolean isRedrawPayments)

{

Blobdata image;

bankPaymAdviceVendTmp.CompanyPhone     = companyInfo.phone();

bankPaymAdviceVendTmp.CompanyTeleFax   = companyInfo.teleFax();

bankPaymAdviceVendTmp.CompanyGiro       = companyInfo.Giro;

bankPaymAdviceVendTmp.CompanyCoRegNum   = companyInfo.CoRegNum;

bankPaymAdviceVendTmp.CompanyAddress   = companyAddress;

bankPaymAdviceVendTmp.VendName         = DirPartyTable::findRec(vendTable.Party).Name;

bankPaymAdviceVendTmp.VendAddress       = LogisticsPostalAddress::addressFromRecId(vendTrans.RemittanceAddress);

bankPaymAdviceVendTmp.PaymAdviceDate   = systemDateGet();

bankPaymAdviceVendTmp.PaymentReference = paymref;

BankPaymAdviceVendTmp.VendAccount       = VendTable.AccountNum;

if (newPaymRefLable)

{

bankPaymAdviceVendTmp.PaymRefLabel = newPaymRefLable;

}

if (!isRedrawPayments)

{

bankPaymAdviceVendTmp.Invoice       = vendTrans.Invoice;

bankPaymAdviceVendTmp.InvoiceDate   = vendTrans.TransDate;

bankPaymAdviceVendTmp.CurrencyCode = vendTrans.CurrencyCode;

bankPaymAdviceVendTmp.AmountCur     = vendTrans.AmountCur;

bankPaymAdviceVendTmp.CashDisc     = this.getCashDiscAmount();

bankPaymAdviceVendTmp.Balance01     = -specTrans.Balance01;

bankPaymAdviceVendTmp.TransDate     = ledgerJournalTrans.TransDate;

if (isSettledVoucherAmount)

{

bankPaymAdviceVendTmp.SettledAmount   = settledAmount;

}

}

else

{

bankPaymAdviceVendTmp.Invoice       = vendTransOriginalInvoice.Invoice;

bankPaymAdviceVendTmp.InvoiceDate   = vendTransOriginalInvoice.TransDate;

bankPaymAdviceVendTmp.CurrencyCode = vendTransOriginalInvoice.CurrencyCode;

bankPaymAdviceVendTmp.AmountCur     = vendTransOriginalInvoice.AmountCur;

bankPaymAdviceVendTmp.CashDisc     = 0;                                   //we only give the discount once

bankPaymAdviceVendTmp.Balance01     = this.paymentAmountCur();

bankPaymAdviceVendTmp.TransDate     = ledgerJournalTrans.TransDate;

if (isSettledVoucherAmount)

{

bankPaymAdviceVendTmp.SettledAmount   = -settledAmount;

}

}

bankPaymAdviceVendTmp.SessionId           = currentSessionId;

bankPaymAdviceVendTmp.Grouping           = groupBy;

bankPaymAdviceVendTmp.SettledCurrencyCode = settledCurrencyCode;

image = CompanyImage::find(companyInfo.dataAreaId, tablenum(CompanyInfo), companyInfo.RecId).Image;

bankPaymAdviceVendTmp.Image = image;

if (image)

{

bankPaymAdviceVendTmp.PrintImage = NoYesCombo::Yes;

}

else

{

bankPaymAdviceVendTmp.PrintImage = NoYesCombo::No;

}

bankPaymAdviceVendTmp.insert();

}

private AmountCur paymentAmountCur()

{

CustVendTransDetails vendTransDetails = new CustVendTransDetails(vendTransOriginalInvoice);

vendTransDetails.setCustVendTrans(vendTransOriginalInvoice);

return (vendTransOriginalInvoice.AmountCur – vendTransDetails.utilizedCashDisc());

}

protected void preRunModifyContract()

{

Query reportQuery;

 reportQuery = this.parmReportContract().parmQueryContracts().lookup(this.getFirstQueryContractKey());

SrsReportHelper::addParameterValueRangeToQuery(

reportQuery,

tableNum(BankPaymAdviceVendTmp),

fieldNum(BankPaymAdviceVendTmp,SessionId),

this.currentSessionId());

 }

public void processReport(List paramOutPaymlist)

{

boolean ret = false;

boolean calcSum;

VendSettlement vendSettlement;

VendPromissoryNoteInvoice vendPromissoryNoteInvoice;

TaxWithholdTrans taxWithholdTrans;

ListEnumerator li;

VendOutPaymRecord vendOutPaymRecord;

companyInfo = CompanyInfo::find();

companyAddress = companyInfo.postalAddress().Address;

currentSessionId = new xSession().sessionId();

//Delete the previous records if there are existing record corresponding to the current session or there are existing records

//more than two days old.

delete_from bankPaymAdviceVendTmp where ((bankPaymAdviceVendTmp.SessionId == currentSessionId) ||

(bankPaymAdviceVendTmp.DateOfCreation < today() – 2));

li = paramOutPaymlist.getEnumerator();

while (li.moveNext())

{

vendOutPaymRecord = li.current();

ledgerJournalTrans = vendOutPaymRecord.parmCustVendPaym().ledgerJournalTrans();

vendTable = VendTable::findByCompany(vendOutPaymRecord.parmCustVendPaym().ledgerJournalTrans().Company,

vendOutPaymRecord.parmCustVendPaym().ledgerJournalTrans().parmAccount());

this.setPaymReferenceStr();

calcSum = true;

// print a line for each SpecTrans record for the journal line

while select crosscompany specTrans

where specTrans.SpecRecId == ledgerJournalTrans.RecId &&

specTrans.SpecCompany == ledgerJournalTrans.company() &&

specTrans.SpecTableId == tableNum(LedgerJournalTrans) &&

specTrans.RefTableId == tableNum(VendTransOpen)

{

// The report prints from three global table buffers so we need to populate

// the remaining two prior to sending them to the report for printing

vendTransOpen = specTrans.vendTransOpen();

vendTrans = vendTransOpen.vendTrans();

// Remove tax from the payment amount

changecompany (vendTrans.company())

{

select sum(InvoiceTaxWithholdAmount) from taxWithholdTrans

where taxWithholdTrans.JournalNum == ledgerJournalTrans.JournalNum &&

taxWithholdTrans.Invoice   == vendTrans.Invoice;

//vendTrans.AmountCur and spectrans.Balance01 are negative numbers while taxes are postive

vendTrans.AmountCur += taxWithholdTrans.InvoiceTaxWithholdAmount;

specTrans.Balance01 += taxWithholdTrans.InvoiceTaxWithholdAmount;

}

ret = true;

if (calcSum)

{

settledAmount = ledgerJournalTrans.amount();

settledCurrencyCode =ledgerJournalTrans.displayCurrencyCode();

groupBy = vendOutPaymRecord.parmCustVendPaym().ledgerJournalTrans().parmAccount() + settledCurrencyCode;

calcSum = false;

this.insertBankPaymAdviceVendTmp(true,false);

// Sourabh Khosla (Stoneridge Software Inc.) 04/27/2014 FDD:06 (Email Automation) : Start

// Responsible for sending print reports based on the current vendTable account record.

select * from BankPaymAdviceVendTmp

join vendTable where BankPaymAdviceVendTmp.vendAccount == VendTable.accountNum;

{

this.initPrintMgmtReportRun();

printMgmtReportRun.load(BankPaymAdviceVendTmp,BankPaymAdviceVendTmp,CompanyInfo::languageId());

this.outputReports();

}

sleep(1500); // Gives the time to render the report

delete_from BankPaymAdviceVendTmp;

// Sourabh Khosla (Stoneridge Software Inc.) : End

}

else

{

this.insertBankPaymAdviceVendTmp(false,false);

}

}

if (!ret)

{

// this section is run when generating payments from a redraw

// The promissory note has already been settled which is why the noter invoice exists.

// All the data in this select is from the same company and is based on the payment.

while   select specTrans

where   specTrans.SpecTableId == tableNum(LedgerJournalTrans) &&

specTrans.SpecRecId   == ledgerJournalTrans.RecId &&

specTrans.SpecCompany == ledgerJournalTrans.company()

join   TransRecId,RecId from vendSettlement

where   vendSettlement.RecId == specTrans.RefRecId

join   RecId from vendTrans

where   vendTrans.RecId == vendSettlement.TransRecId

join   PromissoryNoteId,InvoiceCompany,InvoiceVoucher,InvoiceDate,InvoiceId from vendPromissoryNoteInvoice

where   vendPromissoryNoteInvoice.PromissoryNoteId == vendTrans.PromissoryNoteID

{

// Here we get the trans settled which may be in a different company and the

// promissory note invoice has the company to look in captured.

changecompany(vendPromissoryNoteInvoice.InvoiceCompany)

{

select firstonly vendTransOriginalInvoice

where vendTransOriginalInvoice.Voucher   == vendPromissoryNoteInvoice.InvoiceVoucher &&

vendTransOriginalInvoice.TransDate == vendPromissoryNoteInvoice.InvoiceDate   &&

vendTransOriginalInvoice.Invoice   == vendPromissoryNoteInvoice.InvoiceId;

}

if (calcSum)

{

settledAmount = ledgerJournalTrans.amount();

settledCurrencyCode =ledgerJournalTrans.displayCurrencyCode();

groupBy = vendOutPaymRecord.parmCustVendPaym().ledgerJournalTrans().parmAccount() + settledCurrencyCode;

calcSum = false;

this.insertBankPaymAdviceVendTmp(true,true);

}

else

{

this.insertBankPaymAdviceVendTmp(false,true);

}

}

}

}

}

protected void runPrintMgmt()

{

 vendOutPaym = this.parmArgs().caller();

 vendOutPaymList = this.parmArgs().parmObject();

 this.processReport(vendOutPaymList);

 }

private void setPaymReferenceStr()

{

str tmp;

tmp = ledgerJournalTrans.PaymReference;

if (ledgerJournalTrans.BankChequeNum)

{

tmp = ledgerJournalTrans.BankChequeNum;

newPaymRefLable = “@SYS22495” ;

}

 

if (ledgerJournalTrans.BankPromissoryNoteNum)

{

tmp = ledgerJournalTrans.BankPromissoryNoteNum;

newPaymRefLable = “@SYS71440” ;

}

 

if (tmp)

{

if (paymRef != ”)

{

paymRef += ‘,’ + tmp;

}

else

{

paymRef = tmp;

}

}

}

public static void main(Args _args)

{

SrsPrintMgmtController controller = new FDBankPaymAdviceVendController();

controller.parmReportName(#ReportName);

controller.parmArgs(_args);

controller.parmShowDialog(false);

controller.startOperation();

}

********** 

 

This is a supporting class for the new node type.

class PrintMgmtNode_BankPaymAdvice extends PrintMgmtNode

{

}

protected str getDisplayCaptionImplementation(Common _tableBuffer)

{

;

return(strfmt(“@SYS108944”, _tableBuffer.caption()));

}

public List getDocumentTypes()

{

List docTypes;

;

docTypes = new List(Types::Enum);

if (isConfigurationkeyEnabled(configurationkeynum(LogisticsBasic)))

{

docTypes.addEnd(PrintMgmtDocumentType::BankPaymAdvice);

}

return docTypes;

}

public int getIconImageResNum()

{

#resAppl

;

return #ImagePrintManagementTrans;

}

public PrintMgmtNodeType getNodeType()

{

;

return PrintMgmtNodeType::BankPaymAdvice;

}

public RefTableId getReferencedTableId()

{

;

return tablenum(BankPaymAdviceVendTmp);

}

Modified Classes:

class PrintMgmtNode, construct method

//Stoneridge Software LL April 2014 Begin

case PrintMgmtNodeType::BankPaymAdvice:

return new PrintMgmtNode_BankPaymAdvice();

//Stoneridge Software LL April 2014 End

class PrintMgmtHierarchy_Purch

 …

getNodesImplementation

//Stoneridge Software LL April 2014 Begin

supportedNodes.addEnd(PrintMgmtNodeType::BankPaymAdvice);

//Stoneridge Software End

getParentImplementation

This is an important method for bringing the pieces together. The vendaccount value in the temp table ties back to the parent node (vendTable)

//Stoneridge Software LL April 2014 Begin

case PrintMgmtNodeType::BankPaymAdvice:

 

BankPaymAdviceVendTmp = _nodeInstance.parmReferencedTableBuffer();

 

if (strlen(BankPaymAdviceVendTmp.VendAccount) != 0)

{

result.parmReferencedTableBuffer(BankPaymAdviceVendTmp.selectRefRecord(fieldnum(BankPaymAdviceVendTmp, VendAccount)));

}

else

{

result.parmReferencedTableBuffer(null);

}

 

result.parmNodeDefinition(PrintMgmtNode::construct(PrintMgmtNodeType::VendTable));

 

break;

//Stoneridge Software LL April 2014End

class VendOutPaym_NACHA

 

class Declaration

DialogField         paymAdviceUsePrintMgmt; //Stoneridge Software LL April 2014

UsePrintMgmt       paymAdvUsePrintMgmt;   //Stoneridge Software LL April 2014

 

Dialog method

dialog.addGroup(“@SYS54502”);

//Stoneridge Software LL April 2014 Begin

paymAdviceUsePrintMgmt = dialog.addfieldvalue(enumStr(NoYes),NoYes::Yes,”@FDA27″);

//Stoneridge Software LL April 2014 End

getFromDialog method

paymAdvUsePrintMgmt = paymAdviceUsePrintMgmt.value();

 

public void printPaymAdvice()

{

Args               args = new Args();

args.caller(this);

args.parmObject(this.getOutPaymRecords());

args.parmEnumType(enumNum(NoYes));

args.parmEnum(paymAdvUsePrintMgmt); //Stoneridge Software LL April 2014

new MenuFunction(menuitemOutputStr(BankPaymAdviceVend), MenuItemType::Output).run(args);

}

public boolean unpack(container _packedClass)

{

Version version = RunBase::getVersion(_packedClass);

container   base;

boolean     ret;

#LOCALMACRO.CurrentListV3

fileName,

printControlReport,

effectiveEntDate

#ENDMACRO

#define.V3(3)

#LOCALMACRO.CurrentListV4

effectiveEntDate,

paymAdvUsePrintMgmt //Stoneridge Software LL April 2014

#ENDMACRO

}

 

class PrintMgmtNode_Purch

getDocumentTypes

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderInvoice);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderPackingSlip);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderReceiptsList);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderRequisition);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderConfirmationRequest);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQ);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQAccept);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQReject);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQReturn);

//Stoneridge Software LL March 2014 Begin

docTypes.addEnd(PrintMgmtDocumentType::BankPaymAdvice);

//Stoneridge Software End

 

class PrintMgmtNode_VendTable

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderInvoice);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderPackingSlip);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderReceiptsList);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderRequisition);

docTypes.addEnd(PrintMgmtDocumentType::PurchaseOrderConfirmationRequest);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQ);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQAccept);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQReject);

docTypes.addEnd(PrintMgmtDocumentType::PurchRFQReturn);

//Stoneridge Software LL March 2014 Begin

docTypes.addEnd(PrintMgmtDocumentType::BankPaymAdvice);

//Stoneridge Software End

}

 

class PrintMgmtDocType

getDefaultReportFormat()

{

case PrintMgmtDocumentType::PurchaseOrderConfirmationRequest:

return ssrsReportStr(PurchPurchaseOrder, Report);

case PrintMgmtDocumentType::PurchRFQ:

case PrintMgmtDocumentType::PurchRFQAccept:

case PrintMgmtDocumentType::PurchRFQReject:

case PrintMgmtDocumentType::PurchRFQReturn:

return ssrsReportStr(RFQSend, Report);

//Stoneridge Software LL March 2014 Begin

case PrintMgmtDocumentType::BankPaymAdvice:

return ssrsReportStr(BankPaymAdviceVend, Report);

//Stoneridge Software End

Class PrintMgmtDocType

getQueryTableId()

//Stoneridge Software LL March 2014 Begin

case PrintMgmtDocumentType::BankPaymAdvice:

tableId = tableNum(BankPaymAdviceVendTmp);

break;

//Stoneridge Software End

Updates to this post:

  • You will need to modify the menu item to use the new controller class.
  • Also, the new variable for the dialog value “paymAdvUsePrintMgmt” needs to be added to the CurrentList macro in the classDeclaration of VendOutPaym_NACHA.

As noted above, also refer to AX 2012 Print Management Integration Guide for more information on the classes and methods modified here.

2 Comments

  1. Ciaran

    Great post ,
    I used a similar method as this for the bank payment advice report as well!

    I did have an issue trying to get the cheque payment advice report to do similar, i never actually managed to get this working for the cheque payment advice (BankPaymAdviceCheque).

    Did you ever successfully adapt this solution to use with the cheques payment advice report also as i would be interested in seeing what you did as it would greatly help myself looking at the cheque payments.

  2. Taylor Valnes

    Hello Ciaran,

    We’ve never had to do this for the Cheque Payment Advice report, but if you would like us to assist you with it we have our support plans that you could use. Our website is here: https://stoneridgesoftware.com/support/

    Regards,
    Taylor

Submit a Comment

Your email address will not be published. Required fields are marked *

Upcoming Events

february

27feb11:00 am12:00 pmConfab With Stoneridge - Livestream - Cool Features in Dynamics 365 Business Central

march

04mar12:00 pm12:30 pmPower BI & Reporting with Dynamics 365 Customer Engagement

04mar2:00 pm3:00 pmHow to Incorporate the Power Platform with Dynamics 365 Finance and Supply Chain Management

12mar11:00 am12:00 pmConfab With Stoneridge - Livestream - Power Apps AI Builder

18mar12:00 pm1:00 pmMoving from CRM On Prem to the Cloud – Is it worth It?

18mar2:00 pm3:00 pmThe Past, Present, and Future of Dynamics 365 with Machine Learning and Artificial Intelligence

25mar2:00 pm3:00 pmAchieve Total Data Access & Future-Proof Your Reporting

26mar11:00 am12:00 pmConfab With Stoneridge - Livestream - Portals

About Stoneridge
Stoneridge Software is a unique Microsoft Gold Partner, with emphasis on partner. With specialties in Microsoft Dynamics 365, Microsoft Dynamics AX, Microsoft Dynamics NAV, Microsoft Dynamics GP and Microsoft Dynamics CRM, we focus on attracting the most knowledgeable experts in the field to our team, and prioritize delivering stellar solutions with maximum impact for your business. At Stoneridge, we are deeply committed to your results. Each engagement is met with a dedicated team, ready to provide thorough, tailored, and expert service. Based in Minnesota, we intentionally “step into your shoes,” wherever you are. We focus on what you care about, and develop trusting, long-term relationships with our clients.

Subscribe To Our Blog

Sign up to get periodic updates on the latest posts.

Thank you for subscribing!

X