SuiteScript: Preserve Applied Journal Credits When Updating an Invoice
Safely update a NetSuite invoice without losing its applied journal credits by snapshotting applications and restoring missing amounts through a zero-dollar customer payment.
Saving an invoice with SuiteScript can remove a Journal Entry credit that was applied to it. The invoice edit does not need to involve the applied transaction: a programmatic record.load() followed by save() can cause NetSuite to recalculate the invoice and drop the direct journal application.
Customer Payments may remain applied after an invoice edit, but direct Journal Entry applications need special care.
A safe recovery pattern is:
- Snapshot the applied journals before changing the invoice.
- Save the invoice once, and only when it actually changed.
- Check which direct applications disappeared.
- Restore the missing amount through a zero-dollar Customer Payment.
- Record any shortfall or failure for manual follow-up.
This avoids editing the Journal Entry itself.
Why not reload and save the journal?
It is tempting to load the Journal Entry, find the invoice on its application sublist and tick it again. That approach increases the blast radius.
A journal can contain Accounts Receivable credit lines used across multiple customer accounts. Saving the entire journal can revalidate unrelated applications, encounter closed-period restrictions, trigger workflows or conflict with another process. Repairing one invoice should not put every other application on the shared journal at risk.
Using a Customer Payment limits the new transaction to the relevant customer and creates a visible accounting trail.
Snapshot the direct applications with SuiteQL
Before loading the invoice for editing, query its directly applied Journal Entries:
SELECT
NTLL.nextdoc AS journal_id,
JT.tranid AS journal_number,
SUM(NTLL.foreignamount) AS applied_amount
FROM NextTransactionLineLink NTLL
JOIN Transaction JT
ON JT.id = NTLL.nextdoc
WHERE NTLL.previousdoc = ?
AND NTLL.linktype = 'Payment'
AND JT.type = 'Journal'
GROUP BY
NTLL.nextdoc,
JT.tranid
For this relationship:
previousdocis the invoice;nextdocis the Journal Entry;linktypeisPayment.
Run the query before the invoice save. Once the direct link disappears, the original applied amount is no longer available from that relationship.
function getAppliedJournalCredits(invoiceId) {
return query.runSuiteQL({
query: `
SELECT
NTLL.nextdoc AS journal_id,
JT.tranid AS journal_number,
SUM(NTLL.foreignamount) AS applied_amount
FROM NextTransactionLineLink NTLL
JOIN Transaction JT
ON JT.id = NTLL.nextdoc
WHERE NTLL.previousdoc = ?
AND NTLL.linktype = 'Payment'
AND JT.type = 'Journal'
GROUP BY NTLL.nextdoc, JT.tranid
`,
params: [invoiceId],
}).asMappedResults();
}
Normalise IDs and amounts when reading mapped results:
const snapshot = getAppliedJournalCredits(invoiceId).map((row) => ({
journalId: Number(row.journal_id),
journalNumber: String(row.journal_number),
appliedAmount: Math.abs(Number(row.applied_amount) || 0),
}));
Save each invoice only once
Every invoice save is another opportunity for an application to be removed. Avoid a load-and-save cycle for every changed line.
Instead:
- group all requested changes by invoice;
- load each affected invoice once;
- apply every valid change in memory;
- skip the save if nothing changed;
- save once after all changes are complete.
const invoice = record.load({
type: record.Type.INVOICE,
id: invoiceId,
isDynamic: false,
});
let changed = false;
// Apply all required body and line changes here.
// Set changed = true only when a value is actually different.
if (changed) {
invoice.save({
enableSourcing: false,
ignoreMandatoryFields: false,
});
}
If the invoice is not saved, no recovery transaction is required.
Re-query before restoring anything
After the save, run the snapshot query again. Restore only the direct application amount that actually went missing.
This makes a retry safer:
function indexByJournal(rows) {
return new Map(
rows.map((row) => [
Number(row.journal_id),
Math.abs(Number(row.applied_amount) || 0),
]),
);
}
const remaining = indexByJournal(getAppliedJournalCredits(invoiceId));
const missing = snapshot
.map((before) => ({
...before,
missingAmount: Math.max(
0,
before.appliedAmount - (remaining.get(before.journalId) || 0),
),
}))
.filter((row) => row.missingAmount > 0);
Do not blindly replay the complete snapshot. Another process may already have restored part of it.
Restore the credit with a zero-dollar Customer Payment
Transform the customer into a Customer Payment so NetSuite provides the customer’s open invoices and credits:
const payment = record.transform({
fromType: record.Type.CUSTOMER,
fromId: customerId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true,
});
payment.setValue({
fieldId: 'autoapply',
value: false,
});
On the payment:
- the
applysublist contains invoices and other receivables; - the
creditsublist contains available credits, including eligible Journal Entries.
Select the invoice and set the amount:
function findDocumentLine(payment, sublistId, documentId) {
let line = payment.findSublistLineWithValue({
sublistId,
fieldId: 'doc',
value: String(documentId),
});
if (line === -1) {
line = payment.findSublistLineWithValue({
sublistId,
fieldId: 'doc',
value: Number(documentId),
});
}
return line;
}
function applyLine(payment, sublistId, line, amount) {
payment.selectLine({ sublistId, line });
payment.setCurrentSublistValue({
sublistId,
fieldId: 'apply',
value: true,
});
payment.setCurrentSublistValue({
sublistId,
fieldId: 'amount',
value: amount,
});
payment.commitLine({ sublistId });
}
Then select equal amounts on the apply and credit sublists:
const invoiceLine = findDocumentLine(payment, 'apply', invoiceId);
const journalLine = findDocumentLine(payment, 'credit', journalId);
if (invoiceLine === -1 || journalLine === -1) {
throw new Error('Invoice or journal credit is unavailable on the payment');
}
const invoiceDue = Number(
payment.getSublistValue({
sublistId: 'apply',
fieldId: 'due',
line: invoiceLine,
}),
);
const creditDue = Math.abs(Number(
payment.getSublistValue({
sublistId: 'credit',
fieldId: 'due',
line: journalLine,
}),
));
const amountToRestore = Math.min(
missingAmount,
invoiceDue,
creditDue,
);
applyLine(payment, 'apply', invoiceLine, amountToRestore);
applyLine(payment, 'credit', journalLine, amountToRestore);
const paymentId = payment.save();
The applied invoice amount and selected credit amount are equal, so the Customer Payment total remains zero.
One payment can handle several invoices and journal credits for the same customer, provided the total selected on apply equals the total selected on credit. Separate customers require separate payments.
Never force the old amount
An invoice amendment may reduce the invoice balance. The Journal Entry may also have been consumed by another valid transaction between the snapshot and recovery.
Cap the restored amount at the minimum of:
- the amount that disappeared;
- the invoice’s current
dueamount; - the journal credit’s current
dueamount.
If the full amount cannot be restored, leave the remainder unapplied and report it. Forcing an amount beyond either available balance creates a new accounting problem instead of repairing the original one.
Leave an audit trail
Record:
- the invoice ID;
- the Journal Entry ID and number;
- the previous applied amount;
- the amount restored;
- the Customer Payment ID;
- any amount still unapplied;
- the error message when recovery fails.
An invoice note or another account-approved audit record makes the automated repair visible. A useful failure message is explicit:
A portion of the journal credit remains unapplied. Review and reapply it manually.
Do not let a recovery failure disappear into an execution log that Accounts Receivable users cannot see.
Important behaviour after recovery
The relationship is now:
Journal credit -> Customer Payment -> Invoice
It is no longer a direct Journal Entry-to-invoice link. Therefore, the original snapshot query—intentionally restricted to a next transaction of type Journal—returns nothing for the restored amount.
That is expected. Do not interpret it as evidence that the recovery failed, and do not create another payment on a second run.
Production checklist
Before deploying this pattern:
- Test invoices with one and several journal credits.
- Test a journal used across multiple customers.
- Test an amendment that lowers the invoice below the old application.
- Test when part of the journal credit is no longer available.
- Confirm the execution role can see both payment sublists.
- Test closed periods, approval workflows and custom validation scripts.
- Confirm retries do not create duplicate Customer Payments.
- Verify the audit message when only part of an amount can be restored.
The central rule is to preserve the old relationship as data, not by re-saving the shared Journal Entry. Snapshot first, mutate once, restore only what disappeared, and make every shortfall visible.