Has anyone successfully created new customers programmatically in Acumatica?
I'm trying to understand the minimum required fields, whether I can default values from a Customer Class, and what the best practices are.
Yes, I’m aware that import scenarios are the standard approach—but in this case, they’re not sufficient. I’m importing from another accounting system and have custom logic in place for remapping, consolidating, and creating new customers, which import scenarios can’t handle. This process also needs to run automatically every few hours, so I’m looking for a robust, code-based solution.
Any examples or guidance would be appreciated.
Here is my starting point….
// ——————— Create Acumatica Customers (Comprehensive Explicit Creation) ———————
[PXButton(CommitChanges = true)]
[PXUIField(DisplayName = "Create Customers")]
public IEnumerable createCustomers(PXAdapter adapter)
{
PXCache mappingCache = Caches[typeof(SSCustomerMapping)];
// Dictionary to store MappedCustomerID (Acumatica AcctCD) to BAccountID for linking locations
var createdCustomers = new Dictionary<string, int?>();
// --- First Pass: Create Main Customer Records (LocationNumber == "0") ---
foreach (SSCustomerMapping row in Mapping.Select().RowCast<SSCustomerMapping>()
.Where(r => r.Selected == true && r.Processed == false && r.LocationNumber == "0" && !string.IsNullOrWhiteSpace(r.MappedCustomerID)))
{
var customerGraph = PXGraph.CreateInstance<CustomerMaint>();
customerGraph.BAccount.View.Answer = WebDialogResult.Yes; // Suppress dialogs
// Step 1a: Insert Customer (Business Account)
var customer = customerGraph.BAccount.Insert();
customer.AcctCD = row.MappedCustomerID;
customer.AcctName = row.CustomerName ?? "Unnamed";
customer.CustomerClassID = row.CustomerClass ?? "PAPER";
customer.CuryID = row.Currency ?? "USD";
customer.Status = CustomerStatus.Active;
customer.StatementCycleId = "EOM";
customer.StatementType = "O";
// DiscTakenAcctID and DiscTakenSubID will be defaulted by CustomerMaint graph based on CustomerClassID or AR Setup.
// IMPORTANT: Add TermsID, as seen in the working Import Scenario
customer.TermsID = "NET30"; // Provide a default. ENSURE "NET30" exists in your Terms (AR202000)
// Update the customer in cache. At this point, customer.BAccountID is usually assigned.
customer = customerGraph.BAccount.Update(customer);
// Step 1b: Explicitly create and populate default Contact for the main customer
// Ensure a new Contact DAC instance is created for the default contact
var defaultContact = (Contact)customerGraph.Caches<Contact>().Insert(new Contact());
defaultContact.BAccountID = customer.BAccountID; // Link to the customer
defaultContact.Attention = row.Attention;
defaultContact.Phone1 = row.Phone1;
defaultContact.Phone2 = row.Phone2;
defaultContact.EMail = row.Email;
defaultContact = customerGraph.Caches<Contact>().Update(defaultContact);
// Step 1c: Explicitly create and populate default Address for the main customer
// Ensure a new Address DAC instance is created for the default address
var defaultAddress = (Address)customerGraph.Caches<Address>().Insert(new Address());
defaultAddress.BAccountID = customer.BAccountID; // Link to the customer
defaultAddress.AddressLine1 = row.AddressLine1;
defaultAddress.AddressLine2 = row.AddressLine2;
defaultAddress.City = row.City;
defaultAddress.State = row.State;
defaultAddress.PostalCode = row.ZipCode;
defaultAddress.CountryID = row.CountryCode ?? "US";
defaultAddress = customerGraph.Caches<Address>().Update(defaultAddress);
// Step 1d: Explicitly create and populate the default Location for the main customer
// Ensure a new Location DAC instance is created for the default location
var defaultLocation = (Location)customerGraph.Caches<Location>().Insert(new Location());
defaultLocation.BAccountID = customer.BAccountID; // Link to the customer
defaultLocation.LocationCD = "MAIN"; // Typically "MAIN" or "0" for the default location
defaultLocation.Descr = row.CustomerName ?? "Main Location"; // Use customer name as description
defaultLocation.IsDefault = true; // Mark as the default location
// Now, create a contact and address specifically for this default location
// This is crucial to prevent DefLocationExt from generating invalid IDs
var locationDefaultContact = (Contact)customerGraph.Caches<Contact>().Insert(new Contact());
locationDefaultContact.BAccountID = customer.BAccountID;
locationDefaultContact.Attention = row.Attention; // Copy from customer row
locationDefaultContact.Phone1 = row.Phone1;
locationDefaultContact.EMail = row.Email;
locationDefaultContact = customerGraph.Caches<Contact>().Update(locationDefaultContact);
var locationDefaultAddress = (Address)customerGraph.Caches<Address>().Insert(new Address());
locationDefaultAddress.BAccountID = customer.BAccountID;
locationDefaultAddress.AddressLine1 = row.AddressLine1; // Copy from customer row
locationDefaultAddress.AddressLine2 = row.AddressLine2;
locationDefaultAddress.City = row.City;
locationDefaultAddress.State = row.State;
locationDefaultAddress.PostalCode = row.ZipCode;
locationDefaultAddress.CountryID = row.CountryCode ?? "US";
locationDefaultAddress = customerGraph.Caches<Address>().Update(locationDefaultAddress);
// Link the default contact and address to the default location
defaultLocation.DefContactID = locationDefaultContact.ContactID;
defaultLocation.DefAddressID = locationDefaultAddress.AddressID;
defaultLocation = customerGraph.Caches<Location>().Update(defaultLocation);
// Step 1e: Link the newly created default Contact, Address, and Location to the main customer (BAccount)
customer.DefContactID = defaultContact.ContactID;
customer.DefAddressID = defaultAddress.AddressID;
customer.DefLocationID = defaultLocation.LocationID;
customerGraph.BAccount.Update(customer); // Update customer again in cache to reflect all linked IDs
// Step 1f: Save the main customer, its default contact, address, and location
try
{
customerGraph.Save.Press();
PXTrace.WriteInformation($"Successfully created main customer: {customer.AcctCD} (BAccountID: {customer.BAccountID})");
// Store the newly created customer's BAccountID for linking additional locations
if (customer.BAccountID.HasValue)
{
createdCustomers[row.MappedCustomerID] = customer.BAccountID;
}
else
{
PXTrace.WriteError($"Failed to get BAccountID for newly created customer: {row.MappedCustomerID}. Locations for this customer may not be created.");
// Do NOT mark as processed if the main customer wasn't created properly
continue;
}
// Mark as processed in your mapping table (for this main customer row)
row.Processed = true;
row.IsSynced = true;
row.LastSyncedDateTime = PXTimeZoneInfo.Now;
mappingCache.Update(row);
}
catch (Exception ex)
{
PXTrace.WriteError($"Error saving main customer {customer.AcctCD}: {ex.Message}");
// Do NOT mark as processed if there was an error
}
}
// --- Second Pass: Create Additional Location Records (LocationNumber != "0") ---
// Re-selecting to ensure we iterate over records not processed in the first pass
foreach (SSCustomerMapping row in Mapping.Select().RowCast<SSCustomerMapping>()
.Where(r => r.Selected == true && r.Processed == false && r.LocationNumber != "0" && !string.IsNullOrWhiteSpace(r.MappedCustomerID)))
{
// Ensure the parent customer was successfully created in the first pass
if (!createdCustomers.TryGetValue(row.ParentAccountID, out int? parentBAccountID) || !parentBAccountID.HasValue)
{
PXTrace.WriteWarning($"Parent customer with MappedCustomerID '{row.ParentAccountID}' not found for location '{row.MappedCustomerID}'. Skipping location creation.");
continue;
}
var customerGraph = PXGraph.CreateInstance<CustomerMaint>();
customerGraph.BAccount.View.Answer = WebDialogResult.Yes; // Suppress dialogs
// Load the parent customer into the graph context
// This is important for Acumatica's graph logic to associate the new location correctly
customerGraph.BAccount.Current = customerGraph.BAccount.Search<BAccount.bAccountID>(parentBAccountID);
if (customerGraph.BAccount.Current == null)
{
PXTrace.WriteError($"Could not load parent customer BAccountID '{parentBAccountID}' for location '{row.MappedCustomerID}'. Skipping location creation.");
continue;
}
// Step 2a: Create a new Location record
var newLocation = (Location)customerGraph.Caches<Location>().Insert(new Location());
newLocation.BAccountID = parentBAccountID; // Link to the existing parent customer
newLocation.LocationCD = row.LocationNumber;
newLocation.Descr = row.LocationName ?? "Additional Location";
newLocation.IsDefault = false; // This is an *additional* location, not the main default.
// Step 2b: Explicitly create a new Contact for this specific additional location
var newLocationContact = (Contact)customerGraph.Caches<Contact>().Insert(new Contact());
newLocationContact.BAccountID = parentBAccountID; // Link to the same customer
newLocationContact.Attention = row.Attention;
newLocationContact.Phone1 = row.Phone1;
newLocationContact.EMail = row.Email;
newLocationContact = customerGraph.Caches<Contact>().Update(newLocationContact);
// Step 2c: Explicitly create a new Address for this specific additional location
var newLocationAddress = (Address)customerGraph.Caches<Address>().Insert(new Address());
newLocationAddress.BAccountID = parentBAccountID; // Link to the same customer
newLocationAddress.AddressLine1 = row.AddressLine1;
newLocationAddress.AddressLine2 = row.AddressLine2;
newLocationAddress.City = row.City;
newLocationAddress.State = row.State;
newLocationAddress.PostalCode = row.ZipCode;
newLocationAddress.CountryID = row.CountryCode ?? "US";
newLocationAddress = customerGraph.Caches<Address>().Update(newLocationAddress);
// Step 2d: Link the newly created contact and address to the new location
newLocation.DefContactID = newLocationContact.ContactID;
newLocation.DefAddressID = newLocationAddress.AddressID;
// Update the location with its contact and address IDs
customerGraph.Caches<Location>().Update(newLocation);
// Step 2e: Save the additional location and its linked contact/address
try
{
customerGraph.Save.Press();
PXTrace.WriteInformation($"Successfully created location {newLocation.LocationCD} for customer {customerGraph.BAccount.Current?.AcctCD}");
// Mark as processed in your mapping table (for this location row)
row.Processed = true;
row.IsSynced = true;
row.LastSyncedDateTime = PXTimeZoneInfo.Now;
mappingCache.Update(row);
}
catch (Exception ex)
{
PXTrace.WriteError($"Error creating location {row.LocationNumber} for customer {row.MappedCustomerID}: {ex.Message}");
// Do NOT mark as processed if there was an error
}
}
mappingCache.Graph.Actions.PressSave(); // Save final changes to SSCustomerMapping
return adapter.Get();
}