mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
✨ Add ERPNext node (#1604)
* 🚧 Integrated with access token OAuth2 still needs work * 🚧 Removed OAuth2 for now * ⚡ Improvements * ⚡ Improvements * ⚡ Refactor ERPNext node * 🔥 Remove PNG icon * 🔥 Remove leftover comments * 🔨 Catch unavailable resource error * ⚡ Reposition docType for filters * ⚡ Improvements * ⚡ Cleanup Co-authored-by: Rupenieks <ronaldsupenieks96@gmail.com> Co-authored-by: ricardo <ricardoespinoza105@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
12838f26e3
commit
3b00c96643
32
packages/nodes-base/credentials/ERPNextApi.credentials.ts
Normal file
32
packages/nodes-base/credentials/ERPNextApi.credentials.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
NodePropertyTypes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ERPNextApi implements ICredentialType {
|
||||||
|
name = 'erpNextApi';
|
||||||
|
displayName = 'ERPNext API';
|
||||||
|
documentationUrl = 'erpnext';
|
||||||
|
properties = [
|
||||||
|
{
|
||||||
|
displayName: 'API Key',
|
||||||
|
name: 'apiKey',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'API Secret',
|
||||||
|
name: 'apiSecret',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Subdomain',
|
||||||
|
name: 'subdomain',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
placeholder: 'n8n',
|
||||||
|
description: 'ERPNext subdomain. For instance, entering n8n will make the url look like: https://n8n.erpnext.com/.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
463
packages/nodes-base/nodes/ERPNext/DocumentDescription.ts
Normal file
463
packages/nodes-base/nodes/ERPNext/DocumentDescription.ts
Normal file
|
@ -0,0 +1,463 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const documentOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete a document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Retrieve a document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve all documents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update a document.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
description: 'Operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const documentFields = [
|
||||||
|
// ----------------------------------
|
||||||
|
// document: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'DocType',
|
||||||
|
name: 'docType',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocTypes',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'DocType whose documents to retrieve.',
|
||||||
|
placeholder: 'Customer',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Return all items.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 10,
|
||||||
|
description: 'The number of results to return.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Fields',
|
||||||
|
name: 'fields',
|
||||||
|
type: 'multiOptions',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocFilters',
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'docType',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Comma-separated list of fields to return.',
|
||||||
|
placeholder: 'name,country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Filters',
|
||||||
|
name: 'filters',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
description: 'Custom Properties',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Property',
|
||||||
|
name: 'customProperty',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field',
|
||||||
|
name: 'field',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocFields',
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'docType',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operator',
|
||||||
|
name: 'operator',
|
||||||
|
type: 'options',
|
||||||
|
default: 'is',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'IS',
|
||||||
|
value: 'is',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IS NOT',
|
||||||
|
value: 'isNot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IS GREATER',
|
||||||
|
value: 'greater',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IS LESS',
|
||||||
|
value: 'less',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EQUALS, or GREATER',
|
||||||
|
value: 'equalsGreater',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EQUALS, or LESS',
|
||||||
|
value: 'equalsLess',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Value of the operator condition.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: create
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'DocType',
|
||||||
|
name: 'docType',
|
||||||
|
type: 'options',
|
||||||
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocTypes',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'DocType you would like to create.',
|
||||||
|
placeholder: 'Customer',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Properties',
|
||||||
|
name: 'properties',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Property',
|
||||||
|
required: true,
|
||||||
|
default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Property',
|
||||||
|
name: 'customProperty',
|
||||||
|
placeholder: 'Add Property',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field',
|
||||||
|
name: 'field',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocFields',
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'docType',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: get
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'DocType',
|
||||||
|
name: 'docType',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocTypes',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The type of document you would like to get.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Document Name',
|
||||||
|
name: 'documentName',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The name (ID) of document you would like to get.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: delete
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'DocType',
|
||||||
|
name: 'docType',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocTypes',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The type of document you would like to delete.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'delete',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Document Name',
|
||||||
|
name: 'documentName',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The name (ID) of document you would like to get.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'delete',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: update
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'DocType',
|
||||||
|
name: 'docType',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocTypes',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The type of document you would like to update',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Document Name',
|
||||||
|
name: 'documentName',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The name (ID) of document you would like to get.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Properties',
|
||||||
|
name: 'properties',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Property',
|
||||||
|
description: 'Properties of request body.',
|
||||||
|
default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'document',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Property',
|
||||||
|
name: 'customProperty',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field',
|
||||||
|
name: 'field',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDocFields',
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'docType',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
268
packages/nodes-base/nodes/ERPNext/ERPNext.node.ts
Normal file
268
packages/nodes-base/nodes/ERPNext/ERPNext.node.ts
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
documentFields,
|
||||||
|
documentOperations,
|
||||||
|
} from './DocumentDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
erpNextApiRequest,
|
||||||
|
erpNextApiRequestAllItems
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DocumentProperties,
|
||||||
|
processNames,
|
||||||
|
toSQL,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export class ERPNext implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'ERPNext',
|
||||||
|
name: 'erpNext',
|
||||||
|
icon: 'file:erpnext.svg',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||||
|
description: 'Consume ERPNext API',
|
||||||
|
defaults: {
|
||||||
|
name: 'ERPNext',
|
||||||
|
color: '#7574ff',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'erpNextApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Document',
|
||||||
|
value: 'document',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'document',
|
||||||
|
description: 'Resource to consume.',
|
||||||
|
},
|
||||||
|
...documentOperations,
|
||||||
|
...documentFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getDocTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const data = await erpNextApiRequestAllItems.call(this, 'data', 'GET', '/api/resource/DocType', {});
|
||||||
|
const docTypes = data.map(({ name }: { name: string }) => {
|
||||||
|
return { name, value: encodeURI(name) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return processNames(docTypes);
|
||||||
|
},
|
||||||
|
async getDocFilters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const docType = this.getCurrentNodeParameter('docType') as string;
|
||||||
|
const { data } = await erpNextApiRequest.call(this, 'GET', `/api/resource/DocType/${docType}`, {});
|
||||||
|
|
||||||
|
const docFields = data.fields.map(({ label, fieldname }: { label: string, fieldname: string }) => {
|
||||||
|
return ({ name: label, value: fieldname });
|
||||||
|
});
|
||||||
|
|
||||||
|
docFields.unshift({ name: '*', value: '*' });
|
||||||
|
|
||||||
|
return processNames(docFields);
|
||||||
|
},
|
||||||
|
async getDocFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const docType = this.getCurrentNodeParameter('docType') as string;
|
||||||
|
const { data } = await erpNextApiRequest.call(this, 'GET', `/api/resource/DocType/${docType}`, {});
|
||||||
|
|
||||||
|
const docFields = data.fields.map(({ label, fieldname }: { label: string, fieldname: string }) => {
|
||||||
|
return ({ name: label, value: fieldname });
|
||||||
|
});
|
||||||
|
|
||||||
|
return processNames(docFields);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
const body: IDataObject = {};
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
|
||||||
|
const resource = this.getNodeParameter('resource', 0) as string;
|
||||||
|
const operation = this.getNodeParameter('operation', 0) as string;
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/Resources/post_api_resource_Webhook
|
||||||
|
// https://frappeframework.com/docs/user/en/guides/integration/rest_api/manipulating_documents
|
||||||
|
|
||||||
|
if (resource === 'document') {
|
||||||
|
|
||||||
|
// *********************************************************************
|
||||||
|
// document
|
||||||
|
// *********************************************************************
|
||||||
|
|
||||||
|
if (operation === 'get') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: get
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType___DocumentName_
|
||||||
|
|
||||||
|
const docType = this.getNodeParameter('docType', i) as string;
|
||||||
|
const documentName = this.getNodeParameter('documentName', i) as string;
|
||||||
|
|
||||||
|
responseData = await erpNextApiRequest.call(this, 'GET', `/api/resource/${docType}/${documentName}`);
|
||||||
|
responseData = responseData.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType_
|
||||||
|
|
||||||
|
const docType = this.getNodeParameter('docType', i) as string;
|
||||||
|
const endpoint = `/api/resource/${docType}`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
filters,
|
||||||
|
} = this.getNodeParameter('options', i) as {
|
||||||
|
fields: string[],
|
||||||
|
filters: {
|
||||||
|
customProperty: Array<{ field: string, operator: string, value: string }>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// fields=["test", "example", "hi"]
|
||||||
|
if (fields) {
|
||||||
|
if (fields.includes('*')) {
|
||||||
|
qs.fields = JSON.stringify(['*']);
|
||||||
|
} else {
|
||||||
|
qs.fields = JSON.stringify(fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// filters=[["Person","first_name","=","Jane"]]
|
||||||
|
// TODO: filters not working
|
||||||
|
if (filters) {
|
||||||
|
qs.filters = JSON.stringify(filters.customProperty.map((filter) => {
|
||||||
|
return [
|
||||||
|
docType,
|
||||||
|
filter.field,
|
||||||
|
toSQL(filter.operator),
|
||||||
|
filter.value,
|
||||||
|
];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
|
||||||
|
if (!returnAll) {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
qs.limit_page_length = limit;
|
||||||
|
qs.limit_start = 0;
|
||||||
|
responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||||
|
responseData = responseData.data;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
responseData = await erpNextApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (operation === 'create') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: create
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/post_api_resource__DocType_
|
||||||
|
|
||||||
|
const properties = this.getNodeParameter('properties', i) as DocumentProperties;
|
||||||
|
|
||||||
|
if (!properties.customProperty.length) {
|
||||||
|
throw new Error('Please enter at least one property for the document to create.');
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.customProperty.forEach(property => {
|
||||||
|
body[property.field] = property.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const docType = this.getNodeParameter('docType', i) as string;
|
||||||
|
|
||||||
|
responseData = await erpNextApiRequest.call(this, 'POST', `/api/resource/${docType}`, body);
|
||||||
|
responseData = responseData.data;
|
||||||
|
|
||||||
|
} else if (operation === 'delete') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: delete
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/delete_api_resource__DocType___DocumentName_
|
||||||
|
|
||||||
|
const docType = this.getNodeParameter('docType', i) as string;
|
||||||
|
const documentName = this.getNodeParameter('documentName', i) as string;
|
||||||
|
|
||||||
|
responseData = await erpNextApiRequest.call(this, 'DELETE', `/api/resource/${docType}/${documentName}`);
|
||||||
|
|
||||||
|
} else if (operation === 'update') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// document: update
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/put_api_resource__DocType___DocumentName_
|
||||||
|
|
||||||
|
const properties = this.getNodeParameter('properties', i) as DocumentProperties;
|
||||||
|
|
||||||
|
if (!properties.customProperty.length) {
|
||||||
|
throw new Error('Please enter at least one property for the document to update.');
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.customProperty.forEach(property => {
|
||||||
|
body[property.field] = property.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const docType = this.getNodeParameter('docType', i) as string;
|
||||||
|
const documentName = this.getNodeParameter('documentName', i) as string;
|
||||||
|
|
||||||
|
responseData = await erpNextApiRequest.call(this, 'PUT', `/api/resource/${docType}/${documentName}`, body);
|
||||||
|
responseData = responseData.data;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.isArray(responseData)
|
||||||
|
? returnData.push(...responseData)
|
||||||
|
: returnData.push(responseData);
|
||||||
|
|
||||||
|
}
|
||||||
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
|
}
|
||||||
|
}
|
107
packages/nodes-base/nodes/ERPNext/GenericFunctions.ts
Normal file
107
packages/nodes-base/nodes/ERPNext/GenericFunctions.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import {
|
||||||
|
OptionsWithUri,
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
IHookFunctions,
|
||||||
|
IWebhookFunctions
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function erpNextApiRequest(
|
||||||
|
this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions,
|
||||||
|
method: string,
|
||||||
|
resource: string,
|
||||||
|
body: IDataObject = {},
|
||||||
|
query: IDataObject = {},
|
||||||
|
uri?: string,
|
||||||
|
option: IDataObject = {},
|
||||||
|
) {
|
||||||
|
|
||||||
|
const credentials = this.getCredentials('erpNextApi');
|
||||||
|
|
||||||
|
if (credentials === undefined) {
|
||||||
|
throw new Error('No credentials got returned!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let options: OptionsWithUri = {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `token ${credentials.apiKey}:${credentials.apiSecret}`,
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
qs: query,
|
||||||
|
uri: uri || `https://${credentials.subdomain}.erpnext.com${resource}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
options = Object.assign({}, options, option);
|
||||||
|
|
||||||
|
if (!Object.keys(options.body).length) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(options.qs).length) {
|
||||||
|
delete options.qs;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
if (error.statusCode === 403) {
|
||||||
|
throw new Error(
|
||||||
|
`ERPNext error response [${error.statusCode}]: DocType unavailable.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 307) {
|
||||||
|
throw new Error(
|
||||||
|
`ERPNext error response [${error.statusCode}]: Please ensure the subdomain is correct.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorMessages;
|
||||||
|
if (error?.response?.body?._server_messages) {
|
||||||
|
const errors = JSON.parse(error.response.body._server_messages);
|
||||||
|
errorMessages = errors.map((e: string) => JSON.parse(e).message);
|
||||||
|
throw new Error(
|
||||||
|
`ARPNext error response [${error.statusCode}]: ${errorMessages.join('|')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function erpNextApiRequestAllItems(
|
||||||
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
propertyName: string,
|
||||||
|
method: string,
|
||||||
|
resource: string,
|
||||||
|
body: IDataObject,
|
||||||
|
query: IDataObject = {},
|
||||||
|
) {
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
const returnData: any[] = [];
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
query!.limit_start = 0;
|
||||||
|
query!.limit_page_length = 1000;
|
||||||
|
|
||||||
|
do {
|
||||||
|
responseData = await erpNextApiRequest.call(this, method, resource, body, query);
|
||||||
|
returnData.push.apply(returnData, responseData[propertyName]);
|
||||||
|
query!.limit_start += query!.limit_page_length - 1;
|
||||||
|
} while (
|
||||||
|
responseData.data.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
8
packages/nodes-base/nodes/ERPNext/erpnext.svg
Normal file
8
packages/nodes-base/nodes/ERPNext/erpnext.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#7574FF" d="M512,448c0,35.2-28.8,64-64,64H64c-35.2,0-64-28.8-64-64V64C0,28.8,28.8,0,64,0h384c35.2,0,64,28.8,64,64 V448z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M150.483,371.684V141.15c0-15.167,9.534-25.133,23.833-25.133h162.5c13.866,0,20.8,6.933,20.8,18.633v2.6 c0,12.133-6.934,18.633-20.8,18.633h-141.7v78.434h109.634c14.3,0,20.8,6.066,20.8,17.767v1.3c0,12.133-6.934,18.633-20.8,18.633 H195.117v84.934h144.3c13.867,0,20.367,6.066,20.367,17.767v2.167c0,12.566-6.5,19.5-20.367,19.5h-165.1 C160.017,396.384,150.483,386.851,150.483,371.684z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 810 B |
30
packages/nodes-base/nodes/ERPNext/utils.ts
Normal file
30
packages/nodes-base/nodes/ERPNext/utils.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import {
|
||||||
|
flow,
|
||||||
|
sortBy,
|
||||||
|
uniqBy,
|
||||||
|
} from 'lodash';
|
||||||
|
|
||||||
|
export type DocumentProperties = {
|
||||||
|
customProperty: Array<{ field: string; value: string; }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocFields = Array<{ name: string, value: string }>;
|
||||||
|
|
||||||
|
const ensureName = (docFields: DocFields) => docFields.filter(o => o.name);
|
||||||
|
const sortByName = (docFields: DocFields) => sortBy(docFields, ['name']);
|
||||||
|
const uniqueByName = (docFields: DocFields) => uniqBy(docFields, o => o.name);
|
||||||
|
|
||||||
|
export const processNames = flow(ensureName, sortByName, uniqueByName);
|
||||||
|
|
||||||
|
export const toSQL = (operator: string) => {
|
||||||
|
const operators: { [key: string]: string } = {
|
||||||
|
'is': '=',
|
||||||
|
'isNot': '!=',
|
||||||
|
'greater': '>',
|
||||||
|
'less': '<',
|
||||||
|
'equalsGreater': '>=',
|
||||||
|
'equalsLess': '<=',
|
||||||
|
};
|
||||||
|
|
||||||
|
return operators[operator];
|
||||||
|
};
|
|
@ -75,6 +75,7 @@
|
||||||
"dist/credentials/DropboxOAuth2Api.credentials.js",
|
"dist/credentials/DropboxOAuth2Api.credentials.js",
|
||||||
"dist/credentials/EgoiApi.credentials.js",
|
"dist/credentials/EgoiApi.credentials.js",
|
||||||
"dist/credentials/EmeliaApi.credentials.js",
|
"dist/credentials/EmeliaApi.credentials.js",
|
||||||
|
"dist/credentials/ERPNextApi.credentials.js",
|
||||||
"dist/credentials/EventbriteApi.credentials.js",
|
"dist/credentials/EventbriteApi.credentials.js",
|
||||||
"dist/credentials/EventbriteOAuth2Api.credentials.js",
|
"dist/credentials/EventbriteOAuth2Api.credentials.js",
|
||||||
"dist/credentials/FacebookGraphApi.credentials.js",
|
"dist/credentials/FacebookGraphApi.credentials.js",
|
||||||
|
@ -339,6 +340,7 @@
|
||||||
"dist/nodes/Emelia/Emelia.node.js",
|
"dist/nodes/Emelia/Emelia.node.js",
|
||||||
"dist/nodes/Emelia/EmeliaTrigger.node.js",
|
"dist/nodes/Emelia/EmeliaTrigger.node.js",
|
||||||
"dist/nodes/ErrorTrigger.node.js",
|
"dist/nodes/ErrorTrigger.node.js",
|
||||||
|
"dist/nodes/ERPNext/ERPNext.node.js",
|
||||||
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
||||||
"dist/nodes/ExecuteCommand.node.js",
|
"dist/nodes/ExecuteCommand.node.js",
|
||||||
"dist/nodes/ExecuteWorkflow.node.js",
|
"dist/nodes/ExecuteWorkflow.node.js",
|
||||||
|
|
Loading…
Reference in a new issue