mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-23 11:44: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/EgoiApi.credentials.js",
|
||||
"dist/credentials/EmeliaApi.credentials.js",
|
||||
"dist/credentials/ERPNextApi.credentials.js",
|
||||
"dist/credentials/EventbriteApi.credentials.js",
|
||||
"dist/credentials/EventbriteOAuth2Api.credentials.js",
|
||||
"dist/credentials/FacebookGraphApi.credentials.js",
|
||||
|
@ -339,6 +340,7 @@
|
|||
"dist/nodes/Emelia/Emelia.node.js",
|
||||
"dist/nodes/Emelia/EmeliaTrigger.node.js",
|
||||
"dist/nodes/ErrorTrigger.node.js",
|
||||
"dist/nodes/ERPNext/ERPNext.node.js",
|
||||
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
||||
"dist/nodes/ExecuteCommand.node.js",
|
||||
"dist/nodes/ExecuteWorkflow.node.js",
|
||||
|
|
Loading…
Reference in a new issue