Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ NOTE: The `XERO_CLIENT_BEARER_TOKEN` will take precedence over the `XERO_CLIENT_
- `list-accounts`: Retrieve a list of accounts
- `list-contacts`: Retrieve a list of contacts from Xero
- `list-credit-notes`: Retrieve a list of credit notes
- `list-invoices`: Retrieve a list of invoices
- `list-invoices`: Retrieve a list of invoices with advanced filtering
- `list-items`: Retrieve a list of items
- `list-organisation-details`: Retrieve details about an organisation
- `list-profit-and-loss`: Retrieve a profit and loss report
Expand Down
38 changes: 27 additions & 11 deletions src/handlers/list-xero-invoices.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,40 @@ import { formatError } from "../helpers/format-error.js";
import { Invoice } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

export interface ListInvoicesParams {
page?: number;
contactIds?: string[];
invoiceNumbers?: string[];
invoiceIds?: string[];
statuses?: string[];
where?: string;
order?: string;
}

async function getInvoices(
invoiceNumbers: string[] | undefined,
contactIds: string[] | undefined,
page: number,
params: ListInvoicesParams,
): Promise<Invoice[]> {
await xeroClient.authenticate();

const {
page = 1,
contactIds,
invoiceNumbers,
invoiceIds,
statuses,
where,
order = "UpdatedDateUTC DESC",
} = params;

const invoices = await xeroClient.accountingApi.getInvoices(
xeroClient.tenantId,
undefined, // ifModifiedSince
undefined, // where
"UpdatedDateUTC DESC", // order
undefined, // iDs
where, // where
order, // order
invoiceIds, // iDs
invoiceNumbers, // invoiceNumbers
contactIds, // contactIDs
undefined, // statuses
statuses, // statuses
page,
false, // includeArchived
false, // createdByMyApp
Expand All @@ -36,12 +54,10 @@ async function getInvoices(
* List all invoices from Xero
*/
export async function listXeroInvoices(
page: number = 1,
contactIds?: string[],
invoiceNumbers?: string[],
params: ListInvoicesParams,
): Promise<XeroClientResponse<Invoice[]>> {
try {
const invoices = await getInvoices(invoiceNumbers, contactIds, page);
const invoices = await getInvoices(params);

return {
result: invoices,
Expand Down
98 changes: 86 additions & 12 deletions src/tools/list/list-invoices.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,97 @@ import { formatLineItem } from "../../helpers/format-line-item.js";

const ListInvoicesTool = CreateXeroTool(
"list-invoices",
"List invoices in Xero. This includes Draft, Submitted, and Paid invoices. \
Ask the user if they want to see invoices for a specific contact, \
invoice number, or to see all invoices before running. \
Ask the user if they want the next page of invoices after running this tool \
if 10 invoices are returned. \
If they want the next page, call this tool again with the next page number \
and the contact or invoice number if one was provided in the previous call.",
`List invoices in Xero with advanced filtering capabilities.

⚠️ CRITICAL: To filter by invoice Type, you MUST use the 'where' parameter:
- Type=="ACCREC": Sales invoices (customer invoices, accounts receivable)
- Type=="ACCPAY": Bills (purchase invoices, supplier invoices, accounts payable)
There is NO separate 'type' or 'types' parameter - you MUST use where=Type=="ACCREC" or where=Type=="ACCPAY"

This tool supports multiple filtering methods:
1. Simple filters: contactIds, invoiceNumbers, invoiceIds, statuses
2. Advanced 'where' parameter (REQUIRED for Type, dates, amounts, contact names, complex queries)

AI AGENT DECISION GUIDE - Which parameter to use:
- "Type ACCREC" or "sales invoices" → where=Type=="ACCREC"
- "Type ACCPAY" or "bills" → where=Type=="ACCPAY"
- "Status authorised" alone → statuses=["AUTHORISED"]
- "Type ACCREC AND status authorised" → where=Type=="ACCREC" AND Status=="AUTHORISED"
- Date filtering → where=Date>=DateTime(2024,01,01)
- Amount filtering → where=AmountDue>1000
- Contact name → where=Contact.Name=="ABC Ltd"
- Multiple statuses only → statuses=["AUTHORISED","PAID"]
- Specific invoice IDs → invoiceIds=["id1","id2"]

WHEN TO USE THE 'WHERE' PARAMETER (available fields):
- Type: where=Type=="ACCREC" (sales) or Type=="ACCPAY" (bills)
- Status: where=Status=="AUTHORISED" (DRAFT, SUBMITTED, DELETED, AUTHORISED, PAID, VOIDED)
- Contact.Name: where=Contact.Name=="ABC Limited"
- Date: where=Date>=DateTime(2020,01,01) (supports >, >=, <, <=)
- DueDate: where=DueDate<DateTime(2024,01,01) (supports >, >=, <, <=)
- AmountDue: where=AmountDue>=1000 (supports >, >=, <, <=)
- Reference, InvoiceNumber, Contact.ContactID, etc.

COMMON QUERY EXAMPLES:
- "Show sales invoices" → where=Type=="ACCREC"
- "Show bills" → where=Type=="ACCPAY"
- "Authorised sales invoices" → where=Type=="ACCREC" AND Status=="AUTHORISED"
- "Overdue bills" → where=Type=="ACCPAY" AND DueDate<DateTime(2024,12,01) AND Status=="AUTHORISED"
- "Invoices from January 2024" → where=Date>=DateTime(2024,01,01) AND Date<DateTime(2024,02,01)
- "Large unpaid invoices" → where=AmountDue>10000 AND Status=="AUTHORISED"

For filtering by multiple IDs or statuses, prefer using the dedicated array parameters
(invoiceIds, statuses, contactIds) over the where filter for better performance.

Ask the user if they want the next page after returning 10 invoices.`,
{
page: z.number(),
contactIds: z.array(z.string()).optional(),
page: z.number().optional().describe("Page number for pagination (default: 1)"),
contactIds: z
.array(z.string())
.optional()
.describe("Filter by contact IDs (comma-separated list for optimal performance)"),
invoiceNumbers: z
.array(z.string())
.optional()
.describe("If provided, invoice line items will also be returned"),
.describe("Filter by invoice numbers. When provided, line items will also be returned"),
invoiceIds: z
.array(z.string())
.optional()
.describe("Filter by invoice IDs (comma-separated list for optimal performance)"),
statuses: z
.array(z.string())
.optional()
.describe(
"Filter by invoice statuses (comma-separated list). Valid values: DRAFT, SUBMITTED, DELETED, AUTHORISED, PAID, VOIDED",
),
where: z
.string()
.optional()
.describe(
"REQUIRED for filtering by invoice Type (ACCREC/ACCPAY), dates, amounts, contact names, or complex queries. " +
"Common patterns: Type==\"ACCREC\" (sales invoices), Type==\"ACCPAY\" (bills), " +
"Type==\"ACCREC\" AND Status==\"AUTHORISED\", Date>=DateTime(2024,01,01), AmountDue>1000, Contact.Name==\"ABC Ltd\". " +
"Range operators: >, >=, <, <=. Logical operators: AND, OR",
),
order: z
.string()
.optional()
.describe(
"Order by field. Optimized fields: InvoiceId, UpdatedDateUTC, Date. Default: 'UpdatedDateUTC DESC'",
),
},
async ({ page, contactIds, invoiceNumbers }) => {
const response = await listXeroInvoices(page, contactIds, invoiceNumbers);
async (params) => {
const { page, contactIds, invoiceNumbers, invoiceIds, statuses, where, order } = params;
const response = await listXeroInvoices({
page,
contactIds,
invoiceNumbers,
invoiceIds,
statuses,
where,
order,
});

if (response.error !== null) {
return {
content: [
Expand Down