diff --git a/packages/nodes-base/credentials/XeroOAuth2Api.credentials.ts b/packages/nodes-base/credentials/XeroOAuth2Api.credentials.ts new file mode 100644 index 0000000000..2db47c13de --- /dev/null +++ b/packages/nodes-base/credentials/XeroOAuth2Api.credentials.ts @@ -0,0 +1,51 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'offline_access', + 'accounting.transactions', + 'accounting.settings', + 'accounting.contacts', +]; + +export class XeroOAuth2Api implements ICredentialType { + name = 'xeroOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Xero OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://login.xero.com/identity/connect/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://identity.xero.com/connect/token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Xero/ContactDescription.ts b/packages/nodes-base/nodes/Xero/ContactDescription.ts new file mode 100644 index 0000000000..418aef44ac --- /dev/null +++ b/packages/nodes-base/nodes/Xero/ContactDescription.ts @@ -0,0 +1,838 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'create a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + +/* -------------------------------------------------------------------------- */ +/* contact:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Full name of contact/organisation', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Account Number', + name: 'accountNumber', + type: 'string', + default: '', + description: 'A user defined account number', + }, + // { + // displayName: 'Addresses', + // name: 'addressesUi', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: '', + // placeholder: 'Add Address', + // options: [ + // { + // name: 'addressesValues', + // displayName: 'Address', + // values: [ + // { + // displayName: 'Type', + // name: 'type', + // type: 'options', + // options: [ + // { + // name: 'PO Box', + // value: 'POBOX', + // }, + // { + // name: 'Street', + // value: 'STREET', + // }, + // ], + // default: '', + // }, + // { + // displayName: 'Line 1', + // name: 'line1', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Line 2', + // name: 'line2', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'City', + // name: 'city', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Region', + // name: 'region', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Postal Code', + // name: 'postalCode', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Country', + // name: 'country', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Attention To', + // name: 'attentionTo', + // type: 'string', + // default: '', + // }, + // ], + // }, + // ], + // }, + { + displayName: 'Bank Account Details', + name: 'bankAccountDetails', + type: 'string', + default: '', + description: 'Bank account number of contact', + }, + { + displayName: 'Contact Number', + name: 'contactNumber', + type: 'string', + default: '', + description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems', + }, + { + displayName: 'Contact Status', + name: 'contactStatus', + type: 'options', + options: [ + { + name: 'Active', + value: 'ACTIVE', + description: 'The Contact is active and can be used in transactions', + }, + { + name: 'Archived', + value: 'ARCHIVED', + description: 'The Contact is archived and can no longer be used in transactions', + }, + { + name: 'GDPR Request', + value: 'GDPRREQUEST', + description: 'The Contact is the subject of a GDPR erasure request', + }, + ], + default: '', + description: 'Current status of a contact - see contact status types', + }, + { + displayName: 'Default Currency', + name: 'defaultCurrency', + type: 'string', + default: '', + description: 'Default currency for raising invoices against contact', + }, + { + displayName: 'Email', + name: 'emailAddress', + type: 'string', + default: '', + description: 'Email address of contact person (umlauts not supported) (max length = 255)', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of contact person (max length = 255)', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of contact person (max length = 255)', + }, + // { + // displayName: 'Phones', + // name: 'phonesUi', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: '', + // placeholder: 'Add Phone', + // options: [ + // { + // name: 'phonesValues', + // displayName: 'Phones', + // values: [ + // { + // displayName: 'Type', + // name: 'type', + // type: 'options', + // options: [ + // { + // name: 'Default', + // value: 'DEFAULT', + // }, + // { + // name: 'DDI', + // value: 'DDI', + // }, + // { + // name: 'Mobile', + // value: 'MOBILE', + // }, + // { + // name: 'Fax', + // value: 'FAX', + // }, + // ], + // default: '', + // }, + // { + // displayName: 'Number', + // name: 'phoneNumber', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Area Code', + // name: 'phoneAreaCode', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Country Code', + // name: 'phoneCountryCode', + // type: 'string', + // default: '', + // }, + // ], + // }, + // ], + // }, + { + displayName: 'Purchase Default Account Code', + name: 'purchasesDefaultAccountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + }, + default: '', + description: 'The default purchases account code for contacts', + }, + { + displayName: 'Sales Default Account Code', + name: 'salesDefaultAccountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + }, + default: '', + description: 'The default sales account code for contacts', + }, + { + displayName: 'Skype', + name: 'skypeUserName', + type: 'string', + default: '', + description: 'Skype user name of contact', + }, + { + displayName: 'Tax Number', + name: 'taxNumber', + type: 'string', + default: '', + description: 'Tax number of contact', + }, + { + displayName: 'Xero Network Key', + name: 'xeroNetworkKey', + type: 'string', + default: '', + description: 'Store XeroNetworkKey for contacts', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + }, +/* -------------------------------------------------------------------------- */ +/* contact:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include Archived', + name: 'includeArchived', + type: 'boolean', + default: false, + description: `Contacts with a status of ARCHIVED will be included in the response`, + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'string', + placeholder: 'contactID', + default: '', + description: 'Order by any element returned', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Asc', + value: 'ASC', + }, + { + name: 'Desc', + value: 'DESC', + }, + ], + default: '', + description: 'Sort order', + }, + { + displayName: 'Where', + name: 'where', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")', + default: '', + description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. Examples Here`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Account Number', + name: 'accountNumber', + type: 'string', + default: '', + description: 'A user defined account number', + }, + // { + // displayName: 'Addresses', + // name: 'addressesUi', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: '', + // placeholder: 'Add Address', + // options: [ + // { + // name: 'addressesValues', + // displayName: 'Address', + // values: [ + // { + // displayName: 'Type', + // name: 'type', + // type: 'options', + // options: [ + // { + // name: 'PO Box', + // value: 'POBOX', + // }, + // { + // name: 'Street', + // value: 'STREET', + // }, + // ], + // default: '', + // }, + // { + // displayName: 'Line 1', + // name: 'line1', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Line 2', + // name: 'line2', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'City', + // name: 'city', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Region', + // name: 'region', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Postal Code', + // name: 'postalCode', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Country', + // name: 'country', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Attention To', + // name: 'attentionTo', + // type: 'string', + // default: '', + // }, + // ], + // }, + // ], + // }, + { + displayName: 'Bank Account Details', + name: 'bankAccountDetails', + type: 'string', + default: '', + description: 'Bank account number of contact', + }, + { + displayName: 'Contact Number', + name: 'contactNumber', + type: 'string', + default: '', + description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems', + }, + { + displayName: 'Contact Status', + name: 'contactStatus', + type: 'options', + options: [ + { + name: 'Active', + value: 'ACTIVE', + description: 'The Contact is active and can be used in transactions', + }, + { + name: 'Archived', + value: 'ARCHIVED', + description: 'The Contact is archived and can no longer be used in transactions', + }, + { + name: 'GDPR Request', + value: 'GDPRREQUEST', + description: 'The Contact is the subject of a GDPR erasure request', + }, + ], + default: '', + description: 'Current status of a contact - see contact status types', + }, + { + displayName: 'Default Currency', + name: 'defaultCurrency', + type: 'string', + default: '', + description: 'Default currency for raising invoices against contact', + }, + { + displayName: 'Email', + name: 'emailAddress', + type: 'string', + default: '', + description: 'Email address of contact person (umlauts not supported) (max length = 255)', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of contact person (max length = 255)', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of contact person (max length = 255)', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Full name of contact/organisation', + }, + // { + // displayName: 'Phones', + // name: 'phonesUi', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: '', + // placeholder: 'Add Phone', + // options: [ + // { + // name: 'phonesValues', + // displayName: 'Phones', + // values: [ + // { + // displayName: 'Type', + // name: 'type', + // type: 'options', + // options: [ + // { + // name: 'Default', + // value: 'DEFAULT', + // }, + // { + // name: 'DDI', + // value: 'DDI', + // }, + // { + // name: 'Mobile', + // value: 'MOBILE', + // }, + // { + // name: 'Fax', + // value: 'FAX', + // }, + // ], + // default: '', + // }, + // { + // displayName: 'Number', + // name: 'phoneNumber', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Area Code', + // name: 'phoneAreaCode', + // type: 'string', + // default: '', + // }, + // { + // displayName: 'Country Code', + // name: 'phoneCountryCode', + // type: 'string', + // default: '', + // }, + // ], + // }, + // ], + // }, + { + displayName: 'Purchase Default Account Code', + name: 'purchasesDefaultAccountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + }, + default: '', + description: 'The default purchases account code for contacts', + }, + { + displayName: 'Sales Default Account Code', + name: 'salesDefaultAccountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + }, + default: '', + description: 'The default sales account code for contacts', + }, + { + displayName: 'Skype', + name: 'skypeUserName', + type: 'string', + default: '', + description: 'Skype user name of contact', + }, + { + displayName: 'Tax Number', + name: 'taxNumber', + type: 'string', + default: '', + description: 'Tax number of contact', + }, + { + displayName: 'Xero Network Key', + name: 'xeroNetworkKey', + type: 'string', + default: '', + description: 'Store XeroNetworkKey for contacts', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Xero/GenericFunctions.ts b/packages/nodes-base/nodes/Xero/GenericFunctions.ts new file mode 100644 index 0000000000..840579bf2f --- /dev/null +++ b/packages/nodes-base/nodes/Xero/GenericFunctions.ts @@ -0,0 +1,76 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function xeroApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.xero.com/api.xro/2.0${resource}`, + json: true + }; + try { + if (body.organizationId) { + options.headers = { ...options.headers, 'Xero-tenant-id': body.organizationId }; + delete body.organizationId; + } + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'xeroOAuth2Api', options); + } catch (error) { + let errorMessage; + + if (error.response && error.response.body && error.response.body.Message) { + + errorMessage = error.response.body.Message; + + if (error.response.body.Elements) { + const elementErrors = []; + for (const element of error.response.body.Elements) { + elementErrors.push(element.ValidationErrors.map((error: IDataObject) => error.Message).join('|')); + } + errorMessage = elementErrors.join('-'); + } + // Try to return the error prettier + throw new Error(`Xero error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} + +export async function xeroApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await xeroApiRequest.call(this, method, endpoint, body, query); + query.page++; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData[propertyName].length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Xero/IContactInterface.ts b/packages/nodes-base/nodes/Xero/IContactInterface.ts new file mode 100644 index 0000000000..1fc5eebe6a --- /dev/null +++ b/packages/nodes-base/nodes/Xero/IContactInterface.ts @@ -0,0 +1,44 @@ + +export interface IAddress { + Type?: string; + AddressLine1?: string; + AddressLine2?: string; + City?: string; + Region?: string; + PostalCode?: string; + Country?: string; + AttentionTo?: string; +} + +export interface IPhone { + Type?: string; + PhoneNumber?: string; + PhoneAreaCode?: string; + PhoneCountryCode?: string; +} + +export interface IContact extends ITenantId { + AccountNumber?: string; + Addresses?: IAddress[]; + BankAccountDetails?: string; + ContactId?: string; + ContactNumber?: string; + ContactStatus?: string; + DefaultCurrency?: string; + EmailAddress?: string; + FirstName?: string; + LastName?: string; + Name?: string; + Phones?: IPhone[]; + PurchaseTrackingCategory?: string; + PurchasesDefaultAccountCode?: string; + SalesDefaultAccountCode?: string; + SalesTrackingCategory?: string; + SkypeUserName?: string; + taxNumber?: string; + xeroNetworkKey?: string; +} + +export interface ITenantId { + organizationId?: string; +} diff --git a/packages/nodes-base/nodes/Xero/InvoiceDescription.ts b/packages/nodes-base/nodes/Xero/InvoiceDescription.ts new file mode 100644 index 0000000000..6591adc24c --- /dev/null +++ b/packages/nodes-base/nodes/Xero/InvoiceDescription.ts @@ -0,0 +1,983 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const invoiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a invoice', + }, + { + name: 'Get', + value: 'get', + description: 'Get a invoice', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all invoices', + }, + { + name: 'Update', + value: 'update', + description: 'Update a invoice', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const invoiceFields = [ + +/* -------------------------------------------------------------------------- */ +/* invoice:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Bill', + value: 'ACCPAY', + description: 'Accounts Payable or supplier invoice' + }, + { + name: 'Sales Invoice', + value: 'ACCREC', + description: ' Accounts Receivable or customer invoice' + }, + ], + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'Invoice Type', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'Contact ID', + }, + { + displayName: 'Line Items', + name: 'lineItemsUi', + placeholder: 'Add Line Item', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Line item data', + options: [ + { + name: 'lineItemsValues', + displayName: 'Line Item', + values: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A line item with just a description', + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + description: 'LineItem Quantity', + }, + { + displayName: 'Unit Amount', + name: 'unitAmount', + type: 'string', + default: '', + description: 'Lineitem unit amount. By default, unit amount will be rounded to two decimal places.', + }, + { + displayName: 'Item Code', + name: 'itemCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getItemCodes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Account Code', + name: 'accountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Tax Type', + name: 'taxType', + type: 'options', + options: [ + { + name: 'Tax on Purchases', + value: 'INPUT', + }, + { + name: 'Tax Exempt', + value: 'NONE', + }, + { + name: 'Tax on Sales', + value: 'OUTPUT', + }, + { + name: 'Sales Tax on Imports ', + value: 'GSTONIMPORTS', + }, + ], + default: '', + required: true, + description: 'Tax Type', + }, + { + displayName: 'Tax Amount', + name: 'taxAmount', + type: 'string', + default: '', + description: 'The tax amount is auto calculated as a percentage of the line amount based on the tax rate.', + }, + { + displayName: 'Line Amount', + name: 'lineAmount', + type: 'string', + default: '', + description: 'The line amount reflects the discounted price if a DiscountRate has been used', + }, + { + displayName: 'Discount Rate', + name: 'discountRate', + type: 'string', + default: '', + description: 'Percentage discount or discount amount being applied to a line item. Only supported on ACCREC invoices - ACCPAY invoices and credit notes in Xero do not support discounts', + }, + // { + // displayName: 'Tracking', + // name: 'trackingUi', + // placeholder: 'Add Tracking', + // description: 'Any LineItem can have a maximum of 2 TrackingCategory elements.', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: {}, + // options: [ + // { + // name: 'trackingValues', + // displayName: 'Tracking', + // values: [ + // { + // displayName: 'Name', + // name: 'name', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getTrakingCategories', + // loadOptionsDependsOn: [ + // 'organizationId', + // ], + // }, + // default: '', + // description: 'Name of the tracking category', + // }, + // { + // displayName: 'Option', + // name: 'option', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getTrakingOptions', + // loadOptionsDependsOn: [ + // '/name', + // ], + // }, + // default: '', + // description: 'Name of the option', + // }, + // ], + // }, + // ], + // }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Branding Theme ID', + name: 'brandingThemeId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBrandingThemes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Currency', + name: 'currency', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCurrencies', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Currency Rate', + name: 'currencyRate', + type: 'string', + default: '', + description: 'The currency rate for a multicurrency invoice. If no rate is specified, the XE.com day rate is used.', + }, + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + description: 'Date invoice was issued - YYYY-MM-DD. If the Date element is not specified it will default to the current date based on the timezone setting of the organisation', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + description: 'Date invoice is due - YYYY-MM-DD', + }, + { + displayName: 'Expected Payment Date', + name: 'expectedPaymentDate', + type: 'dateTime', + default: '', + description: 'Shown on sales invoices (Accounts Receivable) when this has been set', + }, + { + displayName: 'Invoice Number', + name: 'invoiceNumber', + type: 'string', + default: '', + }, + { + displayName: 'Line Amount Type', + name: 'lineAmountType', + type: 'options', + options: [ + { + name: 'Exclusive', + value: 'Exclusive', + description: 'Line items are exclusive of tax', + }, + { + name: 'Inclusive', + value: 'Inclusive', + description: 'Line items are inclusive tax', + }, + { + name: 'NoTax', + value: 'NoTax', + description: 'Line have no tax', + }, + ], + default: 'Exclusive', + }, + { + displayName: 'Planned Payment Date ', + name: 'plannedPaymentDate', + type: 'dateTime', + default: '', + description: 'Shown on bills (Accounts Payable) when this has been set', + }, + { + displayName: 'Reference', + name: 'reference', + type: 'string', + default: '', + description: 'ACCREC only - additional reference number (max length = 255)', + }, + { + displayName: 'Send To Contact', + name: 'sendToContact', + type: 'boolean', + default: false, + description: 'Whether the invoice in the Xero app should be marked as "sent". This can be set only on invoices that have been approved', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Draft', + value: 'DRAFT', + }, + { + name: 'Submitted', + value: 'SUBMITTED', + }, + { + name: 'Authorised', + value: 'AUTHORISED', + }, + ], + default: 'DRAFT', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'URL link to a source document - shown as "Go to [appName]" in the Xero app', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* invoice:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + }, + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Invoice ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Branding Theme ID', + name: 'brandingThemeId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBrandingThemes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + default: '', + description: 'Contact ID', + }, + { + displayName: 'Currency', + name: 'currency', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCurrencies', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Currency Rate', + name: 'currencyRate', + type: 'string', + default: '', + description: 'The currency rate for a multicurrency invoice. If no rate is specified, the XE.com day rate is used.', + }, + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + description: 'Date invoice was issued - YYYY-MM-DD. If the Date element is not specified it will default to the current date based on the timezone setting of the organisation', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + description: 'Date invoice is due - YYYY-MM-DD', + }, + { + displayName: 'Expected Payment Date', + name: 'expectedPaymentDate', + type: 'dateTime', + default: '', + description: 'Shown on sales invoices (Accounts Receivable) when this has been set', + }, + { + displayName: 'Invoice Number', + name: 'invoiceNumber', + type: 'string', + default: '', + }, + { + displayName: 'Line Amount Type', + name: 'lineAmountType', + type: 'options', + options: [ + { + name: 'Exclusive', + value: 'Exclusive', + description: 'Line items are exclusive of tax', + }, + { + name: 'Inclusive', + value: 'Inclusive', + description: 'Line items are inclusive tax', + }, + { + name: 'NoTax', + value: 'NoTax', + description: 'Line have no tax', + }, + ], + default: 'Exclusive', + }, + { + displayName: 'Line Items', + name: 'lineItemsUi', + placeholder: 'Add Line Item', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + description: 'Line item data', + options: [ + { + name: 'lineItemsValues', + displayName: 'Line Item', + values: [ + { + displayName: 'Line Item ID', + name: 'lineItemId', + type: 'string', + default: '', + description: 'The Xero generated identifier for a LineItem', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A line item with just a description', + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + description: 'LineItem Quantity', + }, + { + displayName: 'Unit Amount', + name: 'unitAmount', + type: 'string', + default: '', + description: 'Lineitem unit amount. By default, unit amount will be rounded to two decimal places.', + }, + { + displayName: 'Item Code', + name: 'itemCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getItemCodes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Account Code', + name: 'accountCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountCodes', + loadOptionsDependsOn: [ + 'organizationId', + ], + }, + default: '', + }, + { + displayName: 'Tax Type', + name: 'taxType', + type: 'options', + options: [ + { + name: 'Tax on Purchases', + value: 'INPUT', + }, + { + name: 'Tax Exempt', + value: 'NONE', + }, + { + name: 'Tax on Sales', + value: 'OUTPUT', + }, + { + name: 'Sales Tax on Imports ', + value: 'GSTONIMPORTS', + }, + ], + default: '', + required: true, + description: 'Tax Type', + }, + { + displayName: 'Tax Amount', + name: 'taxAmount', + type: 'string', + default: '', + description: 'The tax amount is auto calculated as a percentage of the line amount based on the tax rate.', + }, + { + displayName: 'Line Amount', + name: 'lineAmount', + type: 'string', + default: '', + description: 'The line amount reflects the discounted price if a DiscountRate has been used', + }, + { + displayName: 'Discount Rate', + name: 'discountRate', + type: 'string', + default: '', + description: 'Percentage discount or discount amount being applied to a line item. Only supported on ACCREC invoices - ACCPAY invoices and credit notes in Xero do not support discounts', + }, + // { + // displayName: 'Tracking', + // name: 'trackingUi', + // placeholder: 'Add Tracking', + // description: 'Any LineItem can have a maximum of 2 TrackingCategory elements.', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: {}, + // options: [ + // { + // name: 'trackingValues', + // displayName: 'Tracking', + // values: [ + // { + // displayName: 'Name', + // name: 'name', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getTrakingCategories', + // loadOptionsDependsOn: [ + // 'organizationId', + // ], + // }, + // default: '', + // description: 'Name of the tracking category', + // }, + // { + // displayName: 'Option', + // name: 'option', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getTrakingOptions', + // loadOptionsDependsOn: [ + // '/name', + // ], + // }, + // default: '', + // description: 'Name of the option', + // }, + // ], + // }, + // ], + // }, + ], + }, + ], + }, + { + displayName: 'Planned Payment Date ', + name: 'plannedPaymentDate', + type: 'dateTime', + default: '', + description: 'Shown on bills (Accounts Payable) when this has been set', + }, + { + displayName: 'Reference', + name: 'reference', + type: 'string', + default: '', + description: 'ACCREC only - additional reference number (max length = 255)', + }, + { + displayName: 'Send To Contact', + name: 'sendToContact', + type: 'boolean', + default: false, + description: 'Whether the invoice in the Xero app should be marked as "sent". This can be set only on invoices that have been approved', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Draft', + value: 'DRAFT', + }, + { + name: 'Submitted', + value: 'SUBMITTED', + }, + { + name: 'Authorised', + value: 'AUTHORISED', + }, + ], + default: 'DRAFT', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'URL link to a source document - shown as "Go to [appName]" in the Xero app', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* invoice:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + }, + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Invoice ID', + }, +/* -------------------------------------------------------------------------- */ +/* invoice:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTenants', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Created By My App', + name: 'createdByMyApp', + type: 'boolean', + default: false, + description: `When set to true you'll only retrieve Invoices created by your app`, + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'string', + placeholder: 'InvoiceID', + default: '', + description: 'Order by any element returned', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Asc', + value: 'ASC', + }, + { + name: 'Desc', + value: 'DESC', + }, + ], + default: '', + description: 'Sort order', + }, + { + displayName: 'Statuses', + name: 'statuses', + type: 'multiOptions', + options: [ + { + name: 'Draft', + value: 'DRAFT', + }, + { + name: 'Submitted', + value: 'SUBMITTED', + }, + { + name: 'Authorised', + value: 'AUTHORISED', + }, + ], + default: [], + }, + { + displayName: 'Where', + name: 'where', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")', + default: '', + description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. Examples Here`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Xero/InvoiceInterface.ts b/packages/nodes-base/nodes/Xero/InvoiceInterface.ts new file mode 100644 index 0000000000..6d6da63fb9 --- /dev/null +++ b/packages/nodes-base/nodes/Xero/InvoiceInterface.ts @@ -0,0 +1,40 @@ +import { + IDataObject, + } from 'n8n-workflow'; + + export interface ILineItem { + Description?: string; + Quantity?: string; + UnitAmount?: string; + ItemCode?: string; + AccountCode?: string; + LineItemID?: string; + TaxType?: string; + TaxAmount?: string; + LineAmount?: string; + DiscountRate?: string; + Tracking?: IDataObject[]; +} + +export interface IInvoice extends ITenantId { + Type?: string; + LineItems?: ILineItem[]; + Contact?: IDataObject; + Date?: string; + DueDate?: string; + LineAmountType?: string; + InvoiceNumber?: string; + Reference?: string; + BrandingThemeID?: string; + Url?: string; + CurrencyCode?: string; + CurrencyRate?: string; + Status?: string; + SentToContact?: boolean; + ExpectedPaymentDate?: string; + PlannedPaymentDate?: string; +} + +export interface ITenantId { + organizationId?: string; +} diff --git a/packages/nodes-base/nodes/Xero/Xero.node.ts b/packages/nodes-base/nodes/Xero/Xero.node.ts new file mode 100644 index 0000000000..7703bf0c88 --- /dev/null +++ b/packages/nodes-base/nodes/Xero/Xero.node.ts @@ -0,0 +1,681 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + xeroApiRequest, + xeroApiRequestAllItems, +} from './GenericFunctions'; + +import { + invoiceFields, + invoiceOperations +} from './InvoiceDescription'; + +import { + contactFields, + contactOperations, +} from './ContactDescription'; + +import { + IInvoice, + ILineItem, +} from './InvoiceInterface'; + +import { + IContact, + IPhone, + IAddress, +} from './IContactInterface'; + +export class Xero implements INodeType { + description: INodeTypeDescription = { + displayName: 'Xero', + name: 'xero', + icon: 'file:xero.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Xero API', + defaults: { + name: 'Xero', + color: '#13b5ea', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'xeroOAuth2Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Invoice', + value: 'invoice', + }, + ], + default: 'invoice', + description: 'Resource to consume.', + }, + // CONTACT + ...contactOperations, + ...contactFields, + // INVOICE + ...invoiceOperations, + ...invoiceFields, + ], + }; + + methods = { + loadOptions: { + // Get all the item codes to display them to user so that he can + // select them easily + async getItemCodes(this: ILoadOptionsFunctions): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId'); + const returnData: INodePropertyOptions[] = []; + const { Items: items } = await xeroApiRequest.call(this, 'GET', '/items', { organizationId }); + for (const item of items) { + const itemName = item.Description; + const itemId = item.Code; + returnData.push({ + name: itemName, + value: itemId, + }); + } + return returnData; + }, + // Get all the account codes to display them to user so that he can + // select them easily + async getAccountCodes(this: ILoadOptionsFunctions): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId'); + const returnData: INodePropertyOptions[] = []; + const { Accounts: accounts } = await xeroApiRequest.call(this, 'GET', '/Accounts', { organizationId }); + for (const account of accounts) { + const accountName = account.Name; + const accountId = account.Code; + returnData.push({ + name: accountName, + value: accountId, + }); + } + return returnData; + }, + // Get all the tenants to display them to user so that he can + // select them easily + async getTenants(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tenants = await xeroApiRequest.call(this, 'GET', '', {}, {}, 'https://api.xero.com/connections'); + for (const tenant of tenants) { + const tenantName = tenant.tenantName; + const tenantId = tenant.tenantId; + returnData.push({ + name: tenantName, + value: tenantId, + }); + } + return returnData; + }, + // Get all the brading themes to display them to user so that he can + // select them easily + async getBrandingThemes(this: ILoadOptionsFunctions): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId'); + const returnData: INodePropertyOptions[] = []; + const { BrandingThemes: themes } = await xeroApiRequest.call(this, 'GET', '/BrandingThemes', { organizationId }); + for (const theme of themes) { + const themeName = theme.Name; + const themeId = theme.BrandingThemeID; + returnData.push({ + name: themeName, + value: themeId, + }); + } + return returnData; + }, + // Get all the brading themes to display them to user so that he can + // select them easily + async getCurrencies(this: ILoadOptionsFunctions): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId'); + const returnData: INodePropertyOptions[] = []; + const { Currencies: currencies } = await xeroApiRequest.call(this, 'GET', '/Currencies', { organizationId }); + for (const currency of currencies) { + const currencyName = currency.Code; + const currencyId = currency.Description; + returnData.push({ + name: currencyName, + value: currencyId, + }); + } + return returnData; + }, + // Get all the tracking categories to display them to user so that he can + // select them easily + async getTrakingCategories(this: ILoadOptionsFunctions): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId'); + const returnData: INodePropertyOptions[] = []; + const { TrackingCategories: categories } = await xeroApiRequest.call(this, 'GET', '/TrackingCategories', { organizationId }); + for (const category of categories) { + const categoryName = category.Name; + const categoryId = category.TrackingCategoryID; + returnData.push({ + name: categoryName, + value: categoryId, + }); + } + return returnData; + }, + // // Get all the tracking categories to display them to user so that he can + // // select them easily + // async getTrakingOptions(this: ILoadOptionsFunctions): Promise { + // const organizationId = this.getCurrentNodeParameter('organizationId'); + // const name = this.getCurrentNodeParameter('name'); + // const returnData: INodePropertyOptions[] = []; + // const { TrackingCategories: categories } = await xeroApiRequest.call(this, 'GET', '/TrackingCategories', { organizationId }); + // const { Options: options } = categories.filter((category: IDataObject) => category.Name === name)[0]; + // for (const option of options) { + // const optionName = option.Name; + // const optionId = option.TrackingOptionID; + // returnData.push({ + // name: optionName, + // value: optionId, + // }); + // } + // return returnData; + // }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + //https://developer.xero.com/documentation/api/invoices + if (resource === 'invoice') { + if (operation === 'create') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const type = this.getNodeParameter('type', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const contactId = this.getNodeParameter('contactId', i) as string; + const lineItemsValues = ((this.getNodeParameter('lineItemsUi', i) as IDataObject).lineItemsValues as IDataObject[]); + + const body: IInvoice = { + organizationId, + Type: type, + Contact: { ContactID: contactId }, + }; + + if (lineItemsValues) { + const lineItems: ILineItem[] = []; + for (const lineItemValue of lineItemsValues) { + const lineItem: ILineItem = { + Tracking: [], + }; + lineItem.AccountCode = lineItemValue.accountCode as string; + lineItem.Description = lineItemValue.description as string; + lineItem.DiscountRate = lineItemValue.discountRate as string; + lineItem.ItemCode = lineItemValue.itemCode as string; + lineItem.LineAmount = lineItemValue.lineAmount as string; + lineItem.Quantity = (lineItemValue.quantity as number).toString(); + lineItem.TaxAmount = lineItemValue.taxAmount as string; + lineItem.TaxType = lineItemValue.taxType as string; + lineItem.UnitAmount = lineItemValue.unitAmount as string; + // if (lineItemValue.trackingUi) { + // //@ts-ignore + // const { trackingValues } = lineItemValue.trackingUi as IDataObject[]; + // if (trackingValues) { + // for (const trackingValue of trackingValues) { + // const tracking: IDataObject = {}; + // tracking.Name = trackingValue.name as string; + // tracking.Option = trackingValue.option as string; + // lineItem.Tracking!.push(tracking); + // } + // } + // } + lineItems.push(lineItem); + } + body.LineItems = lineItems; + } + + if (additionalFields.brandingThemeId) { + body.BrandingThemeID = additionalFields.brandingThemeId as string; + } + if (additionalFields.currency) { + body.CurrencyCode = additionalFields.currency as string; + } + if (additionalFields.currencyRate) { + body.CurrencyRate = additionalFields.currencyRate as string; + } + if (additionalFields.date) { + body.Date = additionalFields.date as string; + } + if (additionalFields.dueDate) { + body.DueDate = additionalFields.dueDate as string; + } + if (additionalFields.dueDate) { + body.DueDate = additionalFields.dueDate as string; + } + if (additionalFields.expectedPaymentDate) { + body.ExpectedPaymentDate = additionalFields.expectedPaymentDate as string; + } + if (additionalFields.invoiceNumber) { + body.InvoiceNumber = additionalFields.invoiceNumber as string; + } + if (additionalFields.lineAmountType) { + body.LineAmountType = additionalFields.lineAmountType as string; + } + if (additionalFields.plannedPaymentDate) { + body.PlannedPaymentDate = additionalFields.plannedPaymentDate as string; + } + if (additionalFields.reference) { + body.Reference = additionalFields.reference as string; + } + if (additionalFields.sendToContact) { + body.SentToContact = additionalFields.sendToContact as boolean; + } + if (additionalFields.status) { + body.Status = additionalFields.status as string; + } + if (additionalFields.url) { + body.Url = additionalFields.url as string; + } + + responseData = await xeroApiRequest.call(this, 'POST', '/Invoices', body); + responseData = responseData.Invoices; + } + if (operation === 'update') { + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + const organizationId = this.getNodeParameter('organizationId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IInvoice = { + organizationId, + }; + + if (updateFields.lineItemsUi) { + const lineItemsValues = (updateFields.lineItemsUi as IDataObject).lineItemsValues as IDataObject[]; + if (lineItemsValues) { + const lineItems: ILineItem[] = []; + for (const lineItemValue of lineItemsValues) { + const lineItem: ILineItem = { + Tracking: [], + }; + lineItem.AccountCode = lineItemValue.accountCode as string; + lineItem.Description = lineItemValue.description as string; + lineItem.DiscountRate = lineItemValue.discountRate as string; + lineItem.ItemCode = lineItemValue.itemCode as string; + lineItem.LineAmount = lineItemValue.lineAmount as string; + lineItem.Quantity = (lineItemValue.quantity as number).toString(); + lineItem.TaxAmount = lineItemValue.taxAmount as string; + lineItem.TaxType = lineItemValue.taxType as string; + lineItem.UnitAmount = lineItemValue.unitAmount as string; + // if (lineItemValue.trackingUi) { + // //@ts-ignore + // const { trackingValues } = lineItemValue.trackingUi as IDataObject[]; + // if (trackingValues) { + // for (const trackingValue of trackingValues) { + // const tracking: IDataObject = {}; + // tracking.Name = trackingValue.name as string; + // tracking.Option = trackingValue.option as string; + // lineItem.Tracking!.push(tracking); + // } + // } + // } + lineItems.push(lineItem); + } + body.LineItems = lineItems; + } + } + + if (updateFields.type) { + body.Type = updateFields.type as string; + } + if (updateFields.Contact) { + body.Contact = { ContactID: updateFields.contactId as string }; + } + if (updateFields.brandingThemeId) { + body.BrandingThemeID = updateFields.brandingThemeId as string; + } + if (updateFields.currency) { + body.CurrencyCode = updateFields.currency as string; + } + if (updateFields.currencyRate) { + body.CurrencyRate = updateFields.currencyRate as string; + } + if (updateFields.date) { + body.Date = updateFields.date as string; + } + if (updateFields.dueDate) { + body.DueDate = updateFields.dueDate as string; + } + if (updateFields.dueDate) { + body.DueDate = updateFields.dueDate as string; + } + if (updateFields.expectedPaymentDate) { + body.ExpectedPaymentDate = updateFields.expectedPaymentDate as string; + } + if (updateFields.invoiceNumber) { + body.InvoiceNumber = updateFields.invoiceNumber as string; + } + if (updateFields.lineAmountType) { + body.LineAmountType = updateFields.lineAmountType as string; + } + if (updateFields.plannedPaymentDate) { + body.PlannedPaymentDate = updateFields.plannedPaymentDate as string; + } + if (updateFields.reference) { + body.Reference = updateFields.reference as string; + } + if (updateFields.sendToContact) { + body.SentToContact = updateFields.sendToContact as boolean; + } + if (updateFields.status) { + body.Status = updateFields.status as string; + } + if (updateFields.url) { + body.Url = updateFields.url as string; + } + + responseData = await xeroApiRequest.call(this, 'POST', `/Invoices/${invoiceId}`, body); + responseData = responseData.Invoices; + } + if (operation === 'get') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + responseData = await xeroApiRequest.call(this, 'GET', `/Invoices/${invoiceId}`, { organizationId }); + responseData = responseData.Invoices; + } + if (operation === 'getAll') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.statuses) { + qs.statuses = (options.statuses as string[]).join(','); + } + if (options.orderBy) { + qs.order = `${options.orderBy} ${(options.sortOrder === undefined) ? 'DESC' : options.sortOrder}`; + } + if (options.where) { + qs.where = options.where; + } + if (options.createdByMyApp) { + qs.createdByMyApp = options.createdByMyApp as boolean; + } + if (returnAll) { + responseData = await xeroApiRequestAllItems.call(this, 'Invoices', 'GET', '/Invoices', { organizationId }, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = await xeroApiRequest.call(this, 'GET', `/Invoices`, { organizationId }, qs); + responseData = responseData.Invoices; + responseData = responseData.splice(0, limit); + } + } + } + if (resource === 'contact') { + } + if (operation === 'create') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + // const addressesUi = additionalFields.addressesUi as IDataObject; + // const phonesUi = additionalFields.phonesUi as IDataObject; + + const body: IContact = { + Name: name, + }; + + if (additionalFields.accountNumber) { + body.AccountNumber = additionalFields.accountNumber as string; + } + + if (additionalFields.bankAccountDetails) { + body.BankAccountDetails = additionalFields.bankAccountDetails as string; + } + + if (additionalFields.contactNumber) { + body.ContactNumber = additionalFields.contactNumber as string; + } + + if (additionalFields.contactStatus) { + body.ContactStatus = additionalFields.contactStatus as string; + } + + if (additionalFields.defaultCurrency) { + body.DefaultCurrency = additionalFields.defaultCurrency as string; + } + + if (additionalFields.emailAddress) { + body.EmailAddress = additionalFields.emailAddress as string; + } + + if (additionalFields.firstName) { + body.FirstName = additionalFields.firstName as string; + } + + if (additionalFields.lastName) { + body.LastName = additionalFields.lastName as string; + } + + if (additionalFields.purchasesDefaultAccountCode) { + body.PurchasesDefaultAccountCode = additionalFields.purchasesDefaultAccountCode as string; + } + + if (additionalFields.salesDefaultAccountCode) { + body.SalesDefaultAccountCode = additionalFields.salesDefaultAccountCode as string; + } + + if (additionalFields.skypeUserName) { + body.SkypeUserName = additionalFields.skypeUserName as string; + } + + if (additionalFields.taxNumber) { + body.taxNumber = additionalFields.taxNumber as string; + } + + if (additionalFields.xeroNetworkKey) { + body.xeroNetworkKey = additionalFields.xeroNetworkKey as string; + } + + // if (phonesUi) { + // const phoneValues = phonesUi?.phonesValues as IDataObject[]; + // if (phoneValues) { + // const phones: IPhone[] = []; + // for (const phoneValue of phoneValues) { + // const phone: IPhone = {}; + // phone.Type = phoneValue.type as string; + // phone.PhoneNumber = phoneValue.PhoneNumber as string; + // phone.PhoneAreaCode = phoneValue.phoneAreaCode as string; + // phone.PhoneCountryCode = phoneValue.phoneCountryCode as string; + // phones.push(phone); + // } + // body.Phones = phones; + // } + // } + + // if (addressesUi) { + // const addressValues = addressesUi?.addressesValues as IDataObject[]; + // if (addressValues) { + // const addresses: IAddress[] = []; + // for (const addressValue of addressValues) { + // const address: IAddress = {}; + // address.Type = addressValue.type as string; + // address.AddressLine1 = addressValue.line1 as string; + // address.AddressLine2 = addressValue.line2 as string; + // address.City = addressValue.city as string; + // address.Region = addressValue.region as string; + // address.PostalCode = addressValue.postalCode as string; + // address.Country = addressValue.country as string; + // address.AttentionTo = addressValue.attentionTo as string; + // addresses.push(address); + // } + // body.Addresses = addresses; + // } + // } + + responseData = await xeroApiRequest.call(this, 'POST', '/Contacts', { organizationId, Contacts: [body] }); + responseData = responseData.Contacts; + } + if (operation === 'get') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const contactId = this.getNodeParameter('contactId', i) as string; + responseData = await xeroApiRequest.call(this, 'GET', `/Contacts/${contactId}`, { organizationId }); + responseData = responseData.Contacts; + } + if (operation === 'getAll') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.includeArchived) { + qs.includeArchived = options.includeArchived as boolean; + } + if (options.orderBy) { + qs.order = `${options.orderBy} ${(options.sortOrder === undefined) ? 'DESC' : options.sortOrder}`; + } + if (options.where) { + qs.where = options.where; + } + if (returnAll) { + responseData = await xeroApiRequestAllItems.call(this, 'Contacts', 'GET', '/Contacts', { organizationId }, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = await xeroApiRequest.call(this, 'GET', `/Contacts`, { organizationId }, qs); + responseData = responseData.Contacts; + responseData = responseData.splice(0, limit); + } + + } + if (operation === 'update') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const contactId = this.getNodeParameter('contactId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + // const addressesUi = updateFields.addressesUi as IDataObject; + // const phonesUi = updateFields.phonesUi as IDataObject; + + const body: IContact = {}; + + if (updateFields.accountNumber) { + body.AccountNumber = updateFields.accountNumber as string; + } + + if (updateFields.name) { + body.Name = updateFields.name as string; + } + + if (updateFields.bankAccountDetails) { + body.BankAccountDetails = updateFields.bankAccountDetails as string; + } + + if (updateFields.contactNumber) { + body.ContactNumber = updateFields.contactNumber as string; + } + + if (updateFields.contactStatus) { + body.ContactStatus = updateFields.contactStatus as string; + } + + if (updateFields.defaultCurrency) { + body.DefaultCurrency = updateFields.defaultCurrency as string; + } + + if (updateFields.emailAddress) { + body.EmailAddress = updateFields.emailAddress as string; + } + + if (updateFields.firstName) { + body.FirstName = updateFields.firstName as string; + } + + if (updateFields.lastName) { + body.LastName = updateFields.lastName as string; + } + + if (updateFields.purchasesDefaultAccountCode) { + body.PurchasesDefaultAccountCode = updateFields.purchasesDefaultAccountCode as string; + } + + if (updateFields.salesDefaultAccountCode) { + body.SalesDefaultAccountCode = updateFields.salesDefaultAccountCode as string; + } + + if (updateFields.skypeUserName) { + body.SkypeUserName = updateFields.skypeUserName as string; + } + + if (updateFields.taxNumber) { + body.taxNumber = updateFields.taxNumber as string; + } + + if (updateFields.xeroNetworkKey) { + body.xeroNetworkKey = updateFields.xeroNetworkKey as string; + } + + // if (phonesUi) { + // const phoneValues = phonesUi?.phonesValues as IDataObject[]; + // if (phoneValues) { + // const phones: IPhone[] = []; + // for (const phoneValue of phoneValues) { + // const phone: IPhone = {}; + // phone.Type = phoneValue.type as string; + // phone.PhoneNumber = phoneValue.PhoneNumber as string; + // phone.PhoneAreaCode = phoneValue.phoneAreaCode as string; + // phone.PhoneCountryCode = phoneValue.phoneCountryCode as string; + // phones.push(phone); + // } + // body.Phones = phones; + // } + // } + + // if (addressesUi) { + // const addressValues = addressesUi?.addressesValues as IDataObject[]; + // if (addressValues) { + // const addresses: IAddress[] = []; + // for (const addressValue of addressValues) { + // const address: IAddress = {}; + // address.Type = addressValue.type as string; + // address.AddressLine1 = addressValue.line1 as string; + // address.AddressLine2 = addressValue.line2 as string; + // address.City = addressValue.city as string; + // address.Region = addressValue.region as string; + // address.PostalCode = addressValue.postalCode as string; + // address.Country = addressValue.country as string; + // address.AttentionTo = addressValue.attentionTo as string; + // addresses.push(address); + // } + // body.Addresses = addresses; + // } + // } + + responseData = await xeroApiRequest.call(this, 'POST', `/Contacts/${contactId}`, { organizationId, Contacts: [body] }); + responseData = responseData.Contacts; + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Xero/xero.png b/packages/nodes-base/nodes/Xero/xero.png new file mode 100644 index 0000000000..a9d46c10aa Binary files /dev/null and b/packages/nodes-base/nodes/Xero/xero.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 27151efcda..159fdd8cd0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -143,6 +143,7 @@ "dist/credentials/WebflowOAuth2Api.credentials.js", "dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/XeroOAuth2Api.credentials.js", "dist/credentials/ZendeskApi.credentials.js", "dist/credentials/ZendeskOAuth2Api.credentials.js", "dist/credentials/ZohoOAuth2Api.credentials.js", @@ -305,6 +306,7 @@ "dist/nodes/WooCommerce/WooCommerce.node.js", "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", + "dist/nodes/Xero/Xero.node.js", "dist/nodes/Xml.node.js", "dist/nodes/Zendesk/Zendesk.node.js", "dist/nodes/Zendesk/ZendeskTrigger.node.js",