diff --git a/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts index 44a7bc5d04..35d3da16b6 100644 --- a/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts +++ b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts @@ -22,6 +22,7 @@ import { } from 'change-case'; import { + omit, pickBy, } from 'lodash'; @@ -30,7 +31,10 @@ import { } from 'request'; import { + DateFieldsUi, + Option, QuickBooksOAuth2Credentials, + TransactionReport, } from './types'; /** @@ -123,12 +127,22 @@ export async function quickBooksApiRequestAllItems( const maxCount = await getCount.call(this, method, endpoint, qs); - const originalQuery = qs.query; + const originalQuery = qs.query as string; do { qs.query = `${originalQuery} MAXRESULTS ${maxResults} STARTPOSITION ${startPosition}`; responseData = await quickBooksApiRequest.call(this, method, endpoint, qs, body); - returnData.push(...responseData.QueryResponse[capitalCase(resource)]); + try { + const nonResource = originalQuery.split(' ')?.pop(); + if (nonResource === 'CreditMemo' || nonResource === 'Term') { + returnData.push(...responseData.QueryResponse[nonResource]); + } else { + returnData.push(...responseData.QueryResponse[capitalCase(resource)]); + } + } catch (error) { + return []; + } + startPosition += maxResults; } while (maxCount > returnData.length); @@ -273,7 +287,7 @@ export async function loadResource( resourceItems.forEach((resourceItem: { DisplayName: string, Name: string, Id: string }) => { returnData.push({ - name: resourceItem.DisplayName || resourceItem.Name, + name: resourceItem.DisplayName || resourceItem.Name || `Memo ${resourceItem.Id}`, value: resourceItem.Id, }); }); @@ -428,3 +442,63 @@ export function populateFields( }); return body; } + +export const toOptions = (option: string) => ({ name: option, value: option }); + +export const toDisplayName = ({ name, value }: Option) => { + return { name: splitPascalCase(name), value }; +}; + +export const splitPascalCase = (word: string) => { + return word.match(/($[a-z])|[A-Z][^A-Z]+/g)?.join(' '); +}; + +export function adjustTransactionDates( + transactionFields: IDataObject & DateFieldsUi, +): IDataObject { + const dateFieldKeys = [ + 'dateRangeCustom', + 'dateRangeDueCustom', + 'dateRangeModificationCustom', + 'dateRangeCreationCustom', + ] as const; + + if (dateFieldKeys.every(dateField => !transactionFields[dateField])) { + return transactionFields; + } + + let adjusted = omit(transactionFields, dateFieldKeys) as IDataObject; + + dateFieldKeys.forEach(dateFieldKey => { + const dateField = transactionFields[dateFieldKey]; + + if (dateField) { + Object.entries(dateField[`${dateFieldKey}Properties`]).map(([key, value]) => + dateField[`${dateFieldKey}Properties`][key] = value.split('T')[0], + ); + + adjusted = { + ...adjusted, + ...dateField[`${dateFieldKey}Properties`], + }; + } + }); + + return adjusted; +} + +export function simplifyTransactionReport(transactionReport: TransactionReport) { + const columns = transactionReport.Columns.Column.map((column) => column.ColType); + const rows = transactionReport.Rows.Row.map((row) => row.ColData.map(i => i.value)); + + const simplified = []; + for (const row of rows) { + const transaction: { [key: string]: string } = {}; + for (let i = 0; i < row.length; i++) { + transaction[columns[i]] = row[i]; + } + simplified.push(transaction); + } + + return simplified; +} diff --git a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts index 31e4798dfb..2a9f254a5c 100644 --- a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts +++ b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts @@ -28,11 +28,14 @@ import { paymentOperations, purchaseFields, purchaseOperations, + transactionFields, + transactionOperations, vendorFields, vendorOperations, } from './descriptions'; import { + adjustTransactionDates, getRefAndSyncToken, getSyncToken, handleBinaryData, @@ -41,6 +44,7 @@ import { populateFields, processLines, quickBooksApiRequest, + simplifyTransactionReport, } from './GenericFunctions'; import { @@ -52,7 +56,9 @@ import { } from 'lodash'; import { + DateFieldsUi, QuickBooksOAuth2Credentials, + TransactionFields, } from './types'; export class QuickBooks implements INodeType { @@ -114,6 +120,10 @@ export class QuickBooks implements INodeType { name: 'Purchase', value: 'purchase', }, + { + name: 'Transaction', + value: 'transaction', + }, { name: 'Vendor', value: 'vendor', @@ -138,6 +148,8 @@ export class QuickBooks implements INodeType { ...paymentFields, ...purchaseOperations, ...purchaseFields, + ...transactionOperations, + ...transactionFields, ...vendorOperations, ...vendorFields, ], @@ -153,14 +165,26 @@ export class QuickBooks implements INodeType { return await loadResource.call(this, 'preferences'); }, + async getDepartments(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'department'); + }, + async getItems(this: ILoadOptionsFunctions) { return await loadResource.call(this, 'item'); }, + async getMemos(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'CreditMemo'); + }, + async getPurchases(this: ILoadOptionsFunctions) { return await loadResource.call(this, 'purchase'); }, + async getTerms(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'Term'); + }, + async getVendors(this: ILoadOptionsFunctions) { return await loadResource.call(this, 'vendor'); }, @@ -948,6 +972,67 @@ export class QuickBooks implements INodeType { } + } else if (resource === 'transaction') { + + // ********************************************************************* + // transaction + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/transactionlist + + if (operation === 'getReport') { + + // ---------------------------------- + // transaction: getReport + // ---------------------------------- + + const { + columns, + memo, + term, + customer, + vendor, + ...rest + } = this.getNodeParameter('filters', i) as TransactionFields; + + let qs = { ...rest }; + + if (columns?.length) { + qs.columns = columns.join(','); + } + + if (memo?.length) { + qs.memo = memo.join(','); + } + + if (term?.length) { + qs.term = term.join(','); + } + + if (customer?.length) { + qs.customer = customer.join(','); + } + + if (vendor?.length) { + qs.vendor = vendor.join(','); + } + + qs = adjustTransactionDates(qs); + + const endpoint = `/v3/company/${companyId}/reports/TransactionList`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, qs, {}); + + const simplifyResponse = this.getNodeParameter('simple', i, true) as boolean; + + if (!Object.keys(responseData?.Rows).length) { + responseData = []; + } + + if (simplifyResponse && !Array.isArray(responseData)) { + responseData = simplifyTransactionReport(responseData); + } + } + } else if (resource === 'vendor') { // ********************************************************************* diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/TransactionDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/TransactionDescription.ts new file mode 100644 index 0000000000..3485305d76 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/TransactionDescription.ts @@ -0,0 +1,398 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + toDisplayName, + toOptions, +} from '../../GenericFunctions'; + +import { + GROUP_BY_OPTIONS, + PAYMENT_METHODS, + PREDEFINED_DATE_RANGES, + SOURCE_ACCOUNT_TYPES, + TRANSACTION_REPORT_COLUMNS, + TRANSACTION_TYPES, +} from './constants'; + +export const transactionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getReport', + description: 'Operation to perform', + options: [ + { + name: 'Get Report', + value: 'getReport', + }, + ], + displayOptions: { + show: { + resource: [ + 'transaction', + ], + }, + }, + }, +] as INodeProperties[]; + +export const transactionFields = [ + // ---------------------------------- + // transaction: getReport + // ---------------------------------- + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'transaction', + ], + operation: [ + 'getReport', + ], + }, + }, + default: true, + description: 'Return a simplified version of the response instead of the raw data.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'transaction', + ], + operation: [ + 'getReport', + ], + }, + }, + options: [ + { + displayName: 'Accounts Payable Paid', + name: 'appaid', + type: 'options', + default: 'All', + options: ['All', 'Paid', 'Unpaid'].map(toOptions), + }, + { + displayName: 'Accounts Receivable Paid', + name: 'arpaid', + type: 'options', + default: 'All', + options: ['All', 'Paid', 'Unpaid'].map(toOptions), + }, + { + displayName: 'Cleared Status', + name: 'cleared', + type: 'options', + default: 'Reconciled', + options: ['Cleared', 'Uncleared', 'Reconciled', 'Deposited'].map(toOptions), + }, + { + displayName: 'Columns', + name: 'columns', + type: 'multiOptions', + default: '', + description: 'Columns to return', + options: TRANSACTION_REPORT_COLUMNS, + }, + { + displayName: 'Customer', + name: 'customer', + type: 'multiOptions', + default: [], + description: 'Customer to filter results by', + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + }, + { + displayName: 'Date Range (Custom)', + name: 'dateRangeCustom', + placeholder: 'Add Date Range', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Date Range Properties', + name: 'dateRangeCustomProperties', + values: [ + { + displayName: 'Start Date', + name: 'start_date', + type: 'dateTime', + default: '', + description: 'Start date of the date range to filter results by', + }, + { + displayName: 'End Date', + name: 'end_date', + type: 'dateTime', + default: '', + description: 'End date of the date range to filter results by', + }, + ], + }, + ], + }, + { + displayName: 'Date Range (Predefined)', + name: 'date_macro', + type: 'options', + default: 'This Month', + description: 'Predefined date range to filter results by', + options: PREDEFINED_DATE_RANGES.map(toOptions), + }, + { + displayName: 'Date Range for Creation Date (Custom)', + name: 'dateRangeCreationCustom', + placeholder: 'Add Creation Date Range', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Creation Date Range Properties', + name: 'dateRangeCreationCustomProperties', + values: [ + { + displayName: 'Start Creation Date', + name: 'start_createdate', + type: 'dateTime', + default: '', + description: 'Start date of the account creation date range to filter results by', + }, + { + displayName: 'End Creation Date', + name: 'end_createdate', + type: 'dateTime', + default: '', + description: 'End date of the account creation date range to filter results by', + }, + ], + }, + ], + }, + { + displayName: 'Date Range for Creation Date (Predefined)', + name: 'createdate_macro', + type: 'options', + default: 'This Month', + options: PREDEFINED_DATE_RANGES.map(toOptions), + description: 'Predefined report account creation date range', + }, + { + displayName: 'Date Range for Due Date (Custom)', + name: 'dateRangeDueCustom', + placeholder: 'Add Due Date Range', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Due Date Range Properties', + name: 'dateRangeDueCustomProperties', + values: [ + { + displayName: 'Start Due Date', + name: 'start_duedate', + type: 'dateTime', + default: '', + description: 'Start date of the due date range to filter results by', + }, + { + displayName: 'End Due Date', + name: 'end_duedate', + type: 'dateTime', + default: '', + description: 'End date of the due date range to filter results by', + }, + ], + }, + ], + }, + { + displayName: 'Date Range for Due Date (Predefined)', + name: 'duedate_macro', + type: 'options', + default: 'This Month', + description: 'Predefined due date range to filter results by', + options: PREDEFINED_DATE_RANGES.map(toOptions), + }, + { + displayName: 'Date Range for Modification Date (Custom)', + name: 'dateRangeModificationCustom', + placeholder: 'Add Modification Date Range', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Modification Date Range Properties', + name: 'dateRangeModificationCustomProperties', + values: [ + { + displayName: 'Start Modification Date', + name: 'start_moddate', + type: 'dateTime', + default: '', + description: 'Start date of the account modification date range to filter results by', + }, + { + displayName: 'End Modification Date', + name: 'end_moddate', + type: 'dateTime', + default: '', + description: 'End date of the account modification date range to filter results by', + }, + ], + }, + ], + }, + { + displayName: 'Date Range for Modification Date (Predefined)', + name: 'moddate_macro', + type: 'options', + default: 'This Month', + description: 'Predefined account modifiction date range to filter results by', + options: PREDEFINED_DATE_RANGES.map(toOptions), + }, + { + displayName: 'Document Number', + name: 'docnum', + type: 'string', + default: '', + description: 'Transaction document number to filter results by', + }, + { + displayName: 'Department', + name: 'department', + type: 'multiOptions', + default: [], + description: 'Department to filter results by', + typeOptions: { + loadOptionsMethod: 'getDepartments', + }, + }, + { + displayName: 'Group By', + name: 'group_by', + default: 'Account', + type: 'options', + description: 'Transaction field to group results by', + options: GROUP_BY_OPTIONS.map(toOptions), + }, + { + displayName: 'Memo', + name: 'memo', + type: 'multiOptions', + default: [], + description: 'Memo to filter results by', + typeOptions: { + loadOptionsMethod: 'getMemos', + }, + }, + { + displayName: 'Payment Method', + name: 'payment_Method', + type: 'options', + default: 'Cash', + description: 'Payment method to filter results by', + options: PAYMENT_METHODS.map(toOptions), + }, + { + displayName: 'Printed Status', + name: 'printed', + type: 'options', + default: 'Printed', + description: 'Printed state to filter results by', + options: [ + { + name: 'Printed', + value: 'Printed', + }, + { + name: 'To Be Printed', + value: 'To_be_printed', + }, + ], + }, + { + displayName: 'Quick Zoom URL', + name: 'qzurl', + type: 'boolean', + default: true, + description: 'Whether Quick Zoom URL information should be generated', + }, + { + displayName: 'Sort By', + name: 'sort_by', + type: 'options', + default: 'account_name', + description: 'Column to sort results by', + options: TRANSACTION_REPORT_COLUMNS, + }, + { + displayName: 'Sort Order', + name: 'sort_order', + type: 'options', + default: 'Ascend', + options: ['Ascend', 'Descend'].map(toOptions), + }, + { + displayName: 'Term', + name: 'term', + type: 'multiOptions', + default: [], + description: 'Term to filter results by', + typeOptions: { + loadOptionsMethod: 'getTerms', + }, + }, + { + displayName: 'Transaction Amount', + name: 'bothamount', + type: 'number', + default: 0, + typeOptions: { + numberPrecision: 2, + }, + description: 'Monetary amount to filter results by', + }, + { + displayName: 'Transaction Type', + name: 'transaction_type', + type: 'options', + default: 'CreditCardCharge', + description: 'Transaction type to filter results by', + options: TRANSACTION_TYPES.map(toOptions).map(toDisplayName), + }, + + { + displayName: 'Source Account Type', + name: 'source_account_type', + default: 'Bank', + type: 'options', + description: 'Account type to filter results by', + options: SOURCE_ACCOUNT_TYPES.map(toOptions).map(toDisplayName), + }, + { + displayName: 'Vendor', + name: 'vendor', + type: 'multiOptions', + default: [], + description: 'Vendor to filter results by', + typeOptions: { + loadOptionsMethod: 'getVendors', + }, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/constants.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/constants.ts new file mode 100644 index 0000000000..678eb523be --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Transaction/constants.ts @@ -0,0 +1,204 @@ +export const PREDEFINED_DATE_RANGES = [ + 'Today', + 'Yesterday', + 'This Week', + 'Last Week', + 'This Week-to-Date', + 'Last Week-to-Date', + 'Next Week', + 'Next 4 Weeks', + 'This Month', + 'Last Month', + 'This Month-to-Date', + 'Last Month-to-Date', + 'Next Month', + 'This Fiscal Quarter', + 'Last Fiscal Quarter', + 'This Fiscal Quarter-to-Date', + 'Last Fiscal Quarter-to-Date', + 'Next Fiscal Quarter', + 'This Fiscal Year', + 'Last Fiscal Year', + 'This Fiscal Year-to-Date', + 'Last Fiscal Year-to-Date', + 'Next Fiscal Year', +]; + +export const TRANSACTION_REPORT_COLUMNS = [ + { + name: 'Account Name', + value: 'account_name', + }, + { + name: 'Created By', + value: 'create_by', + }, + { + name: 'Create Date', + value: 'create_date', + }, + { + name: 'Customer Message', + value: 'cust_msg', + }, + { + name: 'Department Name', + value: 'dept_name', + }, + { + name: 'Due Date', + value: 'due_date', + }, + { + name: 'Document Number', + value: 'doc_num', + }, + { + name: 'Invoice Date', + value: 'inv_date', + }, + { + name: 'Is Account Payable Paid', + value: 'is_ap_paid', + }, + { + name: 'Is Cleared', + value: 'is_cleared', + }, + { + name: 'Last Modified By', + value: 'last_mod_by', + }, + { + name: 'Memo', + value: 'memo', + }, + { + name: 'Name', + value: 'name', + }, + { + name: 'Other Account', + value: 'other_account', + }, + { + name: 'Payment Method', + value: 'pmt_mthod', + }, + { + name: 'Posting', + value: 'is_no_post', + }, + { + name: 'Printed Status', + value: 'printed', + }, + { + name: 'Sales Customer 1', + value: 'sales_cust1', + }, + { + name: 'Sales Customer 2', + value: 'sales_cust2', + }, + { + name: 'Sales Customer 3', + value: 'sales_cust3', + }, + { + name: 'Term Name', + value: 'term_name', + }, + { + name: 'Tracking Number', + value: 'tracking_num', + }, + { + name: 'Transaction Date', + value: 'tx_date', + }, + { + name: 'Transaction Type', + value: 'txn_type', + }, +]; + +export const PAYMENT_METHODS = [ + 'American Express', + 'Cash', + 'Check', + 'Dinners Club', + 'Discover', + 'Master Card', + 'Visa', +]; + +export const TRANSACTION_TYPES = [ + 'Bill', + 'BillPaymentCheck', + 'BillPaymentCreditCard', + 'BillableCharge', + 'CashPurchase', + 'Charge', + 'Check', + 'Credit', + 'CreditCardCharge', + 'CreditCardCredit', + 'CreditMemo', + 'CreditRefund', + 'Deposit', + 'Estimate', + 'GlobalTaxAdjustment', + 'GlobalTaxPayment', + 'InventoryQuantityAdjustment', + 'Invoice', + 'JournalEntry', + 'PurchaseOrder', + 'ReceivePayment', + 'SalesReceipt', + 'Service Tax Defer', + 'Service Tax Gross Adjustment', + 'Service Tax Partial Utilisation', + 'Service Tax Refund', + 'Service Tax Reversal', + 'Statement', + 'TimeActivity', + 'Transfer', + 'VendorCredit', +]; + +export const SOURCE_ACCOUNT_TYPES = [ + 'AccountsPayable', + 'AccountsReceivable', + 'Bank', + 'CostOfGoodsSold', + 'CreditCard', + 'Equity', + 'Expense', + 'FixedAsset', + 'Income', + 'LongTermLiability', + 'NonPosting', + 'OtherAsset', + 'OtherCurrentAsset', + 'OtherCurrentLiability', + 'OtherExpense', + 'OtherIncome', +]; + +export const GROUP_BY_OPTIONS = [ + 'Account', + 'Customer', + 'Day', + 'Employee', + 'Location', + 'Month', + 'Name', + 'None', + 'Payment Method', + 'Quarter', + 'Transaction Type', + 'Vendor', + 'Week', + 'Year', +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts index 58d4d48173..1f2f748a29 100644 --- a/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts @@ -7,3 +7,4 @@ export * from './Item/ItemDescription'; export * from './Payment/PaymentDescription'; export * from './Vendor/VendorDescription'; export * from './Purchase/PurchaseDescription'; +export * from './Transaction/TransactionDescription'; diff --git a/packages/nodes-base/nodes/QuickBooks/types.d.ts b/packages/nodes-base/nodes/QuickBooks/types.d.ts index 0145bf65af..174aa5bd6c 100644 --- a/packages/nodes-base/nodes/QuickBooks/types.d.ts +++ b/packages/nodes-base/nodes/QuickBooks/types.d.ts @@ -1,3 +1,5 @@ +import { IDataObject } from "n8n-workflow"; + export type QuickBooksOAuth2Credentials = { environment: 'production' | 'sandbox'; oauthTokenData: { @@ -6,3 +8,40 @@ export type QuickBooksOAuth2Credentials = { } }; }; + +export type DateFieldsUi = Partial<{ + dateRangeCustom: DateFieldUi; + dateRangeDueCustom: DateFieldUi; + dateRangeModificationCustom: DateFieldUi; + dateRangeCreationCustom: DateFieldUi; +}>; + +type DateFieldUi = { + [key: string]: { + [key: string]: string; + } +}; + +export type TransactionFields = Partial<{ + columns: string[]; + memo: string[]; + term: string[]; + customer: string[]; + vendor: string[]; +}> & DateFieldsUi & IDataObject; + +export type Option = { name: string, value: string }; + +export type TransactionReport = { + Columns: { + Column: Array<{ + ColTitle: string; + ColType: string; + }> + }; + Rows: { + Row: Array<{ + ColData: Array<{ value: string }>; + }> + }; +};