Creating an Invoice Proposal from a Fee Journal in Dynamics 365 Finance & Operations

This article demonstrates how to create an Invoice Proposal programmatically starting from a Project Fee Journal in Dynamics 365 Finance & Operations.

The solution encapsulates the required logic into a reusable class that can be invoked from X++ code, batch processes, custom services, or integrations.

By leveraging the standard Project Management and Accounting framework, the implementation automatically determines the appropriate Funding Source (when not explicitly provided) and generates the corresponding Invoice Proposal while preserving the standard business logic.

Entry Point

The implementation has been encapsulated in a dedicated class named InvoiceProjInvoiceProposalProcessing.

Creating an invoice proposal becomes extremely simple:

InvoiceProjInvoiceProposalProcessing processing =new InvoiceProjInvoiceProposalProcessing();

processing.parmProjJournalId(journalId);
processing.parmProjFundingSource(projFundingSource);
processing.invoiceProposalCreation();

The only mandatory parameter is the JournalId.

The Funding Source is optional. If it isn’t provided, the class automatically determines the correct one before creating the proposal.

/// <summary>
/// InvoiceProjInvoiceProposalProcessing class
/// </summary>
public class InvoiceProjInvoiceProposalProcessing
{
    ProjJournalId projJournalId;
    ProjFundingSource projFundingSource;
    
    /// <summary>
    /// Get or set ProjJournalId
    /// </summary>
    /// <param name = "_projJournalId">The ProjJournalId to set</param>
    /// <returns>The current ProjJournalId</returns>
    public ProjJournalId parmProjJournalId(ProjJournalId _projJournalId = projJournalId)
    {
        projJournalId = _projJournalId;
        return projJournalId;
    }

    /// <summary>
    /// Get or set ProjFundingSource
    /// </summary>
    /// <param name = "_projFundingSource">The ProjFundingSource to set</param>
    /// <returns>The current ProjFundingSource</returns>
    public ProjFundingSource parmProjFundingSource(ProjFundingSource _projFundingSource = projFundingSource)
    {
        projFundingSource = _projFundingSource;
        return projFundingSource;
    }

    /// <summary>
    /// invoiceProposalCreation
    /// </summary>
    /// <returns>ProjProposalId</returns>
    public ProjProposalId invoiceProposalCreation()
    {
        ProjProposalId proposalId;        
        
        if (!projFundingSource.RecId)
        {
            this.calculateProjFundingSource();
        }

        if ((projJournalId != "") && projFundingSource.RecId)
        {
            proposalId = ProjInvoiceProposalCreateExt::createDataInTmp(projFundingSource.RecId, projJournalId);
            Info("Invoice proposal created nr.: " + proposalId);
        }
        else
        {
            throw error("JournalId or ProjFundingSource missing");
        }

        return proposalId;
	}

    /// <summary>
    /// Calculate ProjFundingSource 
    /// </summary>
    private void calculateProjFundingSource()
    {
        ProjJournalTrans projJournalTrans;
        ProjTable projTable, parentprojTable;
        ProjFundingSourceRefId fundingSourceRecId;
        ProjFundingSource projFundingSourceLoc;

        select firstonly ParentId, defaultfundingsource, ProjInvoiceProjId from projTable
        join ProjId from projJournalTrans
            where projTable.ProjId == projJournalTrans.ProjId
               && ProjJournalTrans.JournalId == projJournalId
               && ProjJournalTrans.LineNum == 1;

        select firstonly ProjInvoiceProjId, ProjGroupId, defaultfundingsource from parentprojTable
        where parentprojTable.ProjId == projTable.ParentId;

        fundingSourceRecId = projTable.DefaultFundingSource != 0 ? projTable.DefaultFundingSource : parentprojTable.DefaultFundingSource;

        if (!fundingSourceRecId)
        {
            select firstonly projFundingSourceLoc
            where projFundingSourceLoc.ContractId == projTable.ProjInvoiceProjId
            && projFundingSourceLoc.CustAccount == projTable.CustAccount
            && projFundingSourceLoc.FundingType == ProjFundingType::Customer;

            if (projFundingSourceLoc)
            {
                fundingSourceRecId = projFundingSourceLoc.RecId;
            }
        }

        if (fundingSourceRecId)
        {
            ProjFundingSource projFundingSourceFin = ProjFundingSource::find(fundingSourceRecId, false);
            if (projFundingSourceFin.RecId)
            {
                projFundingSource = projFundingSourceFin;
            }
        }
    }

}

The class tries to identify the correct funding source by following the project hierarchy.

The logic performs the following steps:

  • Retrieves the project associated with the journal.
  • Checks whether the project has a Default Funding Source.
  • If not found, checks the parent project.
  • If still unavailable, searches for a customer funding source associated with the project’s invoice project.
  • Loads the corresponding ProjFundingSource record.

This makes the component reusable in most scenarios without requiring the caller to know the funding source in advance.

Creating the Invoice Proposal

Once both the JournalId and Funding Source are available, the process delegates the actual creation to a custom helper class:

ProjInvoiceProposalCreateExt::createDataInTmp(projFundingSource.RecId, projJournalId);

This class reproduces the standard Invoice Proposal creation process while allowing the proposal to be generated directly from the transactions belonging to the selected Fee Journal.

The implementation performs the following operations:

  1. Creates the standard ProjInvoiceProposalCreateLines object.
  2. Retrieves the voucher associated with the journal.
  3. Reads all ProjRevenueTrans records generated by that voucher.
  4. Finds the matching ProjRevenueTransSale records for the selected funding source.
  5. Populates the temporary table PSATmpProjProposalTrans.
  6. Calls a custom extension of the standard ProjInvoiceProposalInsertLines class to generate the proposal.

Because the implementation relies almost entirely on the standard framework, all standard business logic is preserved, including:

  • Proposal line creation
  • Revenue processing
  • Proposal totals calculation
  • Retention calculation
  • Standard project validation
/// <summary>
/// ProjInvoiceProposalCreateExt class
/// </summary>
class ProjInvoiceProposalCreateExt extends projInvoiceProposalInsertLines
{

    public void createInvoiceProposals(PSATmpProjProposalTrans _tmpProjProposalTrans)
    {
        int i;
        ProjProposalJour proposalJour;
        ProjTable projTable;

        this.progressInit("@SYS54552", 0);
        while select _tmpProjProposalTrans
        {
            if (i == 0)
            {
                this.parmDefaultDimension(_tmpProjProposalTrans.DefaultDimension);
                this.setProjProposalJour(_tmpProjProposalTrans.ProjInvoiceProjId, _tmpProjProposalTrans.ProjId,
                _tmpProjProposalTrans.FundingSourceRefId, _tmpProjProposalTrans.CurrencyCode,
                _tmpProjProposalTrans.TaxInformation_IN, _tmpProjProposalTrans.FixedExchRate);
                i++;
            }
            ttsbegin;
            this.setProjProposalJourPost(_tmpProjProposalTrans);

            select firstonly RecId from projTable
                where projTable.ProjId == _tmpProjProposalTrans.ProjId &&
                        projTable.ProjInvoiceProjId == _tmpProjProposalTrans.ProjInvoiceProjId
                exists join proposalJour
                    where proposalJour.ProjInvoiceProjId == projTable.ProjInvoiceProjId &&
                            proposalJour.ProposalId == projProposalJour.ProposalId;

            if (projTable.RecId)
            {                
                this.doRevenue(_tmpProjProposalTrans.ProjInvoiceProjId, _tmpProjProposalTrans.RefRecId);                
            }

            if (!this.parmSkipRecalculateProposalTotals())
            {
                this.updateInvoice();

                // When an invoice proposal is generated, the system will
                // automatically create retainage withholding records and/or retainage billing records at the project level.
                this.calcRetention();
            }
            ttscommit;
        }
    }

    public static ProjProposalId createDataInTmp(ProjFundingSourceRefId _fundingSource, JournalId _journalId)
    {
        ProjInvoiceProposalCreateLinesParams proposalCreateLinesParams = ProjInvoiceProposalCreateLinesParams::construct();
        ProjInvoiceProposalCreateLines proposalCreateLines;
        ProjInvoiceProposalInsertLines projInvoiceProposalInsertLines;

        ProjRevenueTrans                projRevenueTrans;
        ProjRevenueTransSale            projRevenueTransSale;        
        PSATmpProjProposalTrans         tmpProjProposalTrans;        
        ProjJournalTrans                projJournalTrans;

        TransDate invoiceDate = DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone());
        ProjProposalId proposalId;        

        proposalCreateLinesParams.parmInvoiceDate(invoiceDate);
        proposalCreateLinesParams.parmInvoiceTypeSelection(ProjInvoiceTypeSelection::Both);		
        proposalCreateLinesParams.parmIncludeSubProjects(true);
        proposalCreateLinesParams.parmIsQueryRevenueTrans(true);

        proposalCreateLines = ProjInvoiceProposalCreateLines::newStandard(proposalCreateLinesParams.pack());
        proposalCreateLines.parmProposalCreateLinesParams().parmInvoiceDate(invoiceDate);

        projInvoiceProposalInsertLines = new ProjInvoiceProposalInsertLines(proposalCreateLines, false);

        select firstonly Voucher from ProjJournalTrans
            where ProjJournalTrans.JournalId == _journalId;

        while select projRevenueTrans
            where projRevenueTrans.VoucherJournal == projJournalTrans.Voucher
        {
            select firstonly projRevenueTransSale
                where projRevenueTransSale.TransId == projRevenueTrans.TransId
                && projRevenueTransSale.FundingSource == _fundingSource;

            if (!projRevenueTransSale.RecId)
            {
                continue;
            }

            tmpProjProposalTrans.clear();
            tmpProjProposalTrans.initFromProjRevenueTrans(projRevenueTrans);
            tmpProjProposalTrans.FundingSourceRefId = _fundingSource;
            tmpProjProposalTrans.LineAmount = projRevenueTransSale.LineAmount * -1;
            tmpProjProposalTrans.SalesPrice = projRevenueTransSale.SalesPrice;
            tmpProjProposalTrans.Selected = true;
            tmpProjProposalTrans.RefRecId = projRevenueTransSale.RecId;
            tmpProjProposalTrans.RefTableId = projRevenueTransSale.TableId;
            tmpProjProposalTrans.insert();
        }

        new ProjInvoiceProposalCreateExt(proposalCreateLines, false).createInvoiceProposals(tmpProjProposalTrans);

        select firstonly projRevenueTrans
            where projRevenueTrans.VoucherJournal == projJournalTrans.Voucher
        join projRevenueTransSale
            where projRevenueTransSale.TransId == projRevenueTrans.TransId
                && projRevenueTransSale.FundingSource == _fundingSource;

        if (projRevenueTrans.RecId && projRevenueTransSale.RecId)
        {
            proposalId = ProjTrans::newProjRevenueTransSale(projRevenueTrans, projRevenueTransSale).proposalId();
        }

        return proposalId;
    }

}

Benefits of This Approach

Encapsulating the entire logic inside a dedicated processing class provides several advantages:

  • Only the JournalId is required.
  • The Funding Source can be calculated automatically.
  • Standard Microsoft business logic is reused instead of duplicated.
  • The solution is easy to invoke from X++, batch jobs, custom services, or integrations.
  • The generated Proposal ID is immediately available for subsequent processing.

Conclusion

Generating an Invoice Proposal from a Fee Journal isn’t directly exposed through a simple standard API, but the underlying framework already contains almost everything required.

By wrapping the standard classes inside a reusable processing component, it’s possible to automate the entire process while keeping the implementation clean, maintainable, and aligned with the standard application.

This approach has proven particularly useful in integration scenarios where invoice proposals must be generated automatically without user interaction.

Lascia un commento