Skip to main content
Question

Project Budget Batch Creation via API

  • November 10, 2025
  • 3 replies
  • 50 views

I’m implementing an integration between Acumatica and a Next.js application. The goal is to automatically create a Project, Project Task, and multiple Project Budget records based on data stored in the Next.js app.

The challenge I’m facing is related to the large volume of budget lines — each project can have thousands of budget items. Using the built-in REST API would require sending thousands of individual requests (one per budget line), which is both inefficient and prone to network or timeout issues. Missing even a few budget items is not acceptable in this scenario.

To address this, I’m exploring a solution that allows batch creation of multiple Project Budget records in a single operation (or at least in smaller batches). My plan is to build a customization project in Acumatica that exposes a custom endpoint designed specifically for this use case.

This endpoint would accept a list of budget items (via JSON) and create them all in one go using a PXGraph or a similar mechanism within Acumatica.

this is what i have so far
 

using System.Collections.Generic;
using PX.Data;
using PX.Objects.PM;
using PX.Objects.IN;
using PX.Objects.GL;

public class ProjectBudgetBatchGraph : PXGraph<ProjectBudgetBatchGraph>
{
public PXSelect<PMBudget> MasterView;

// --- DTO ---
public class BudgetLineDto
{
public string ProjectID { get; set; }
public string ProjectTaskID { get; set; }
public string AccountGroup { get; set; }
public string CostCode { get; set; }
public string Description { get; set; }
public string Type { get; set; }
}

// --- PXAction (this will appear in the Mapped Action list) ---
public PXAction<PMBudget> CreateBudgets;
[PXButton]
[PXUIField(DisplayName = "Create Budgets Batch")]
protected virtual System.Collections.IEnumerable createBudgets(PXAdapter adapter)
{
// Your action logic here
return adapter.Get();
}


// --- The actual method you’ll call from API (FIXED) ---
public void CreateBudgetsAPI(List<BudgetLineDto> lines)
{
foreach (var line in lines)
{
// Perform lookups safely
int? projectId = GetProjectID(line.ProjectID);
int? taskId = GetTaskID(line.ProjectTaskID);
int? accountGroupId = GetAccountGroupID(line.AccountGroup);
int? costCodeId = GetCostCodeID(line.CostCode); // Can be null if optional

// *** CRITICAL VALIDATION ADDED HERE ***
// Acumatica DAC inserts will throw NullReferenceException if mandatory fields are null.
// We proactively check and throw a meaningful exception instead.

if (projectId == null)
{
// Stop the process and inform the user which project CD is missing/invalid
throw new PXException($"Invalid or missing Project ID '{line.ProjectID}' found in input data.");
}

if (taskId == null)
{
// Stop the process and inform the user which task CD is missing/invalid
throw new PXException($"Invalid or missing Task ID '{line.ProjectTaskID}' found in input data.");
}

if (accountGroupId == null)
{
// Stop the process and inform the user which account group CD is missing/invalid
throw new PXException($"Invalid or missing Account Group ID '{line.AccountGroup}' found in input data.");
}


// If validation passes, we create the object
var budget = new PMBudget
{
// We use the validated (non-null) IDs
ProjectID = projectId,
ProjectTaskID = taskId,
AccountGroupID = accountGroupId,
CostCodeID = costCodeId, // This field is assumed optional if it can be null
Description = line.Description,
Type = line.Type == "Income" ? AccountType.Income : AccountType.Expense,
};

MasterView.Insert(budget);
}

// This will now only run if all lines passed validation
Actions.PressSave();
}

// --- Simple lookups (remain the same, they use null-conditional operators safely) ---
private int? GetProjectID(string cd) =>
PXSelect<PMProject, Where<PMProject.contractCD, Equal<Required<PMProject.contractCD>>>>
.Select(this, cd)?.TopFirst?.ContractID;
private int? GetTaskID(string cd) =>
PXSelect<PMTask, Where<PMTask.taskCD, Equal<Required<PMTask.taskCD>>>>
.Select(this, cd)?.TopFirst?.TaskID;
private int? GetAccountGroupID(string cd) =>
PXSelect<PMAccountGroup, Where<PMAccountGroup.groupCD, Equal<Required<PMAccountGroup.groupCD>>>>
.Select(this, cd)?.TopFirst?.GroupID;
private int? GetCostCodeID(string cd) =>
PXSelect<PMCostCode, Where<PMCostCode.costCodeCD, Equal<Required<PMCostCode.costCodeCD>>>>
.Select(this, cd)?.TopFirst?.CostCodeID;
private int? GetInventoryID(string cd) =>
PXSelect<InventoryItem, Where<InventoryItem.inventoryCD, Equal<Required<InventoryItem.inventoryCD>>>>
.Select(this, cd)?.TopFirst?.InventoryID;
}

I’ve successfully validated and published the code, but I’m stuck on how to expose this function through an existing endpoint that I’ve already created for other API calls. I’m also not entirely sure if my current code is implemented correctly.

Where should I look or what steps should I take next? Any guidance or suggestions to point me in the right direction would be greatly appreciated.

Thanks!

3 replies

Forum|alt.badge.img+1

Hi ​@steventrinh1999 

Acumatica REST API doesn’t support batch inserts — there’s no built-in way to send a list of records in a single call. If your data volume is manageable, a straightforward approach works fine:

login  
get data 1
get data 2
...
get data 500
logout

Just make sure to implement throttling to avoid overwhelming the API.

If that’s not feasible, consider these alternatives:

  1. Reverse the flow
    Instead of pushing data from Next.js to Acumatica, let Acumatica pull it.
    You can set up a scheduled process within Acumatica that periodically calls your Next.js service to retrieve new records. 

  2. Webhook-triggered pull
    Similar to option 1, but more event-driven.
    When data is ready, Next.js sends a web-hook notification. Acumatica receives it, triggers a process, and then pulls the data from your service.

  3. File-based exchange (recommended for high volumes)
    Next.js exports data to files, and Acumatica downloads and processes them using import scenarios.
    This is typically the most robust option for large data sets — easier to implement, audit, and troubleshoot.

There’s also a free customization on  Acumatica Marketplace that automates file retrieval from various storage providers and feeds them directly into Acumatica import scenarios. It’s worth exploring if you choose the file-based approach.

Hope this will be helpful.


@aleksejslusar19 

Thank you for your response. I’m currently exploring the option of using an Import Scenario for this integration instead of the REST API.

If you have any documentation or references that could help me set this up, please share them — it would be greatly appreciated.

Thank you again for your assistance!

 


Forum|alt.badge.img+2

Hi ​@steventrinh1999 

There is another option that would allow you to create a Project through the API with multiple Budget entries.

Assuming you are using the Default Endpoint, you could extend the Endpoint and add the fields for the Cost/Revenue Budgets to the Projects screen’s endpoint.  This way, you can specify the Cost/Revenue Budget entries in the same request that creates the Project.  You can also do the same for Tasks as well.

 

 

 

All that being said, if you are creating thousands of Budget entries for each Project, you may still need to add the Budget entries in batches.