UNPKG

n8n-nodes-myob

Version:

Simple n8n node to create sales orders in MYOB Business API with multiple line items

558 lines (557 loc) 30.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Myob = void 0; const MyobApi_request_1 = require("../transport/MyobApi.request"); class Myob { constructor() { this.description = { displayName: 'MYOB Sales Order', name: 'myob', icon: 'file:myob.png', group: ['output'], version: 1, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], description: 'Create sales orders in MYOB Business API', defaults: { name: 'MYOB Sales Order' }, credentials: [ { name: 'myobOAuth2Api', required: true }, ], properties: [ { displayName: 'Customer UID', name: 'customerUid', type: 'string', default: '', required: true, description: 'The unique ID of the customer (GUID format)', }, { displayName: 'Tax Inclusive', name: 'isTaxInclusive', type: 'boolean', default: true, description: 'Whether prices include tax', }, { displayName: 'Journal Memo', name: 'journalMemo', type: 'string', default: '', description: 'Internal memo for the journal entry (optional)', }, { displayName: 'Comment', name: 'comment', type: 'string', default: '', description: 'Comment for the sales order (optional)', }, { displayName: 'Purchase Order Number', name: 'purchaseOrderNumber', type: 'string', default: '', description: 'Purchase order reference number (optional)', }, { displayName: 'Shipping Address Line 1', name: 'shippingLine1', type: 'string', default: '', description: 'First line of shipping address (optional)', }, { displayName: 'Shipping Address Line 2', name: 'shippingLine2', type: 'string', default: '', description: 'Second line of shipping address (optional)', }, { displayName: 'Shipping Address Line 3', name: 'shippingLine3', type: 'string', default: '', description: 'Third line of shipping address (optional)', }, { displayName: 'Shipping Address Line 4', name: 'shippingLine4', type: 'string', default: '', description: 'Fourth line of shipping address (optional)', }, { displayName: 'Shipping Address Line 5', name: 'shippingLine5', type: 'string', default: '', description: 'Fifth line of shipping address (optional)', }, { displayName: 'Shipping SKU', name: 'shippingSku', type: 'string', default: '', description: 'SKU for shipping line item (e.g., P&P1, P&P2) - will be added as last line item if provided', }, { displayName: 'Shipping Price', name: 'shippingPrice', type: 'number', default: 0, description: 'Price for shipping line item - will be added as last line item if Shipping SKU is provided', }, { displayName: 'Shipping Description', name: 'shippingDescription', type: 'string', default: '', description: 'Description for shipping line item (optional - uses MYOB item default if not provided)', }, { displayName: 'Input Method', name: 'inputMethod', type: 'options', default: 'ui', description: 'How to specify the line items', options: [ { name: 'UI Form', value: 'ui', description: 'Add line items using the form below' }, { name: 'JSON Array', value: 'json', description: 'Provide line items as a JSON array (for dynamic data)' }, ], }, { displayName: 'Line Items', name: 'lineItems', type: 'fixedCollection', typeOptions: { multipleValues: true }, default: [{}], required: true, displayOptions: { show: { inputMethod: ['ui'] } }, description: 'Line items for the sales order', options: [ { name: 'item', displayName: 'Line Item', values: [ { displayName: 'SKU (Item Number)', name: 'sku', type: 'string', default: '', required: true, description: 'The SKU/Item Number - will automatically lookup the MYOB Item UID' }, { displayName: 'Quantity', name: 'quantity', type: 'number', default: 1, required: true }, { displayName: 'Unit Price', name: 'unitPrice', type: 'number', default: 0, required: true }, { displayName: 'Tax Code UID', name: 'taxCodeUid', type: 'string', default: '', description: 'The unique ID of the tax code (optional - uses item default if not specified)' }, { displayName: 'Description', name: 'description', type: 'string', default: '', description: 'Optional description override (optional - uses item default if not specified)' }, ], }, ], }, { displayName: 'Default SKU', name: 'defaultSku', type: 'string', default: 'DEFAULT-ITEM', required: true, displayOptions: { show: { inputMethod: ['json'] } }, description: 'Default SKU to use when line items have no SKU, empty SKU, or when SKU lookup fails', }, { displayName: 'Default Tax Code UID', name: 'defaultTaxCodeUid', type: 'string', default: '', displayOptions: { show: { inputMethod: ['json'] } }, description: 'Default Tax Code UID to use for all line items (optional - uses item default if not specified)', }, { displayName: 'Line Items Data', name: 'lineItemsData', type: 'json', default: '[]', required: true, displayOptions: { show: { inputMethod: ['json'] } }, description: 'Raw line items array from Shopify webhook or other source. Will extract sku, quantity, price automatically', placeholder: 'Paste Shopify line_items array or use expression like {{ $json.line_items }}', }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; // Process all input data as a single sales order if (items.length === 0) { throw new Error('No input data received'); } // Use the first item for parameters, ignore additional items to prevent multiple executions const i = 0; try { // Get credentials to build the base URL const credentials = await this.getCredentials('myobOAuth2Api'); const baseUrl = `https://api.myob.com/accountright/${credentials.companyFileGuid}`; const customerUid = this.getNodeParameter('customerUid', i); const isTaxInclusive = this.getNodeParameter('isTaxInclusive', i); const journalMemo = this.getNodeParameter('journalMemo', i); const comment = this.getNodeParameter('comment', i); const purchaseOrderNumber = this.getNodeParameter('purchaseOrderNumber', i); const shippingLine1 = this.getNodeParameter('shippingLine1', i); const shippingLine2 = this.getNodeParameter('shippingLine2', i); const shippingLine3 = this.getNodeParameter('shippingLine3', i); const shippingLine4 = this.getNodeParameter('shippingLine4', i); const shippingLine5 = this.getNodeParameter('shippingLine5', i); const shippingSku = this.getNodeParameter('shippingSku', i); const shippingPrice = this.getNodeParameter('shippingPrice', i); const shippingDescription = this.getNodeParameter('shippingDescription', i); const inputMethod = this.getNodeParameter('inputMethod', i); // Clean up purchase order number by removing # symbol const cleanedPurchaseOrderNumber = purchaseOrderNumber ? purchaseOrderNumber.replace(/^#/, '').trim() : ''; // Validate required fields if (!customerUid) { throw new Error('Customer UID is required'); } // Get line items based on input method let lineItemsArray = []; let defaultSku = ''; let defaultTaxCodeUid = ''; if (inputMethod === 'json') { const rawLineItemsData = this.getNodeParameter('lineItemsData', i); defaultSku = this.getNodeParameter('defaultSku', i); defaultTaxCodeUid = this.getNodeParameter('defaultTaxCodeUid', i); try { let rawData; // Handle both string (JSON) and already-parsed object/array data if (typeof rawLineItemsData === 'string') { rawData = JSON.parse(rawLineItemsData); } else if (Array.isArray(rawLineItemsData)) { rawData = rawLineItemsData; } else if (typeof rawLineItemsData === 'object' && rawLineItemsData !== null) { // If it's an object but not an array, it might be wrapped rawData = rawLineItemsData; } else { throw new Error('Line Items Data must be a JSON string or array'); } if (!Array.isArray(rawData)) { throw new Error('Line Items Data must be an array'); } // Transform Shopify/raw data to standardized format lineItemsArray = rawData.map((shopifyItem) => { // Handle empty SKU strings specifically (Shopify can have sku: "") const itemSku = (shopifyItem.sku && shopifyItem.sku.trim() !== '') ? shopifyItem.sku.trim() : defaultSku; // Use current_quantity if available, fallback to quantity, then to 1 const itemQuantity = shopifyItem.current_quantity || shopifyItem.quantity || 1; // Parse price from string to number const itemPrice = parseFloat(shopifyItem.price || shopifyItem.unit_price || '0'); // Calculate discount amount from Shopify discount_allocations let discountAmount = 0; if (shopifyItem.discount_allocations && Array.isArray(shopifyItem.discount_allocations)) { discountAmount = shopifyItem.discount_allocations.reduce((total, discount) => { const discountValue = parseFloat(discount.amount || '0'); return total + discountValue; }, 0); } else if (shopifyItem.total_discount) { // Fallback to total_discount if discount_allocations not available discountAmount = parseFloat(shopifyItem.total_discount || '0'); } // Build description from available fields let itemDescription = shopifyItem.title || shopifyItem.name || ''; if (shopifyItem.variant_title && shopifyItem.variant_title.trim() !== '') { itemDescription = `${itemDescription} - ${shopifyItem.variant_title}`.trim(); } return { sku: itemSku, quantity: itemQuantity, unitPrice: itemPrice, discountAmount: discountAmount, taxCodeUid: defaultTaxCodeUid, description: itemDescription || shopifyItem.description || '' }; }); } catch (error) { throw new Error(`Invalid Line Items Data: ${error.message}`); } } else { const lineItemsData = this.getNodeParameter('lineItems', i); if (!lineItemsData || !lineItemsData.item || lineItemsData.item.length === 0) { throw new Error('At least one line item is required'); } lineItemsArray = lineItemsData.item; } if (lineItemsArray.length === 0) { throw new Error('At least one line item is required'); } // Build lines array with SKU lookup const lines = []; for (const item of lineItemsArray) { let itemUid = ''; let itemDescription = item.description || ''; // Look up Item UID from SKU with fallback to default SKU let actualSku = item.sku; if (item.sku && item.sku.trim() !== '') { try { const skuEncoded = encodeURIComponent(`'${item.sku}'`); const lookupUrl = `/Inventory/Item?$filter=Number eq ${skuEncoded}`; const lookupResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', lookupUrl, {}, {}, baseUrl); if (lookupResponse && lookupResponse.Items && lookupResponse.Items.length > 0) { itemUid = lookupResponse.Items[0].UID; // Use MYOB description if no override provided if (!itemDescription || itemDescription.trim() === '') { itemDescription = lookupResponse.Items[0].Description || ''; } } else { // SKU not found, fallback to default SKU console.log(`SKU '${item.sku}' not found in MYOB, falling back to default SKU '${defaultSku}'`); actualSku = defaultSku; if (defaultSku && defaultSku.trim() !== '') { const defaultSkuEncoded = encodeURIComponent(`'${defaultSku}'`); const defaultLookupUrl = `/Inventory/Item?$filter=Number eq ${defaultSkuEncoded}`; const defaultLookupResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', defaultLookupUrl, {}, {}, baseUrl); if (defaultLookupResponse && defaultLookupResponse.Items && defaultLookupResponse.Items.length > 0) { itemUid = defaultLookupResponse.Items[0].UID; // Use MYOB description if no override provided if (!itemDescription || itemDescription.trim() === '') { itemDescription = defaultLookupResponse.Items[0].Description || ''; } } else { throw new Error(`Default SKU '${defaultSku}' not found in MYOB`); } } else { throw new Error(`SKU '${item.sku}' not found in MYOB and no default SKU provided`); } } } catch (error) { // If it's a network/API error (not SKU not found), still try default SKU if (error.message.includes('not found in MYOB')) { throw error; // Re-throw our custom not found errors } else { console.log(`Failed to lookup SKU '${item.sku}': ${error.message}, falling back to default SKU '${defaultSku}'`); actualSku = defaultSku; if (defaultSku && defaultSku.trim() !== '') { const defaultSkuEncoded = encodeURIComponent(`'${defaultSku}'`); const defaultLookupUrl = `/Inventory/Item?$filter=Number eq ${defaultSkuEncoded}`; const defaultLookupResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', defaultLookupUrl, {}, {}, baseUrl); if (defaultLookupResponse && defaultLookupResponse.Items && defaultLookupResponse.Items.length > 0) { itemUid = defaultLookupResponse.Items[0].UID; // Use MYOB description if no override provided if (!itemDescription || itemDescription.trim() === '') { itemDescription = defaultLookupResponse.Items[0].Description || ''; } } else { throw new Error(`Default SKU '${defaultSku}' not found in MYOB`); } } else { throw new Error(`Failed to lookup SKU '${item.sku}': ${error.message}, and no default SKU provided`); } } } } else { // No SKU provided, use default SKU console.log(`No SKU provided for item, using default SKU '${defaultSku}'`); actualSku = defaultSku; if (defaultSku && defaultSku.trim() !== '') { const defaultSkuEncoded = encodeURIComponent(`'${defaultSku}'`); const defaultLookupUrl = `/Inventory/Item?$filter=Number eq ${defaultSkuEncoded}`; const defaultLookupResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', defaultLookupUrl, {}, {}, baseUrl); if (defaultLookupResponse && defaultLookupResponse.Items && defaultLookupResponse.Items.length > 0) { itemUid = defaultLookupResponse.Items[0].UID; // Use MYOB description if no override provided if (!itemDescription || itemDescription.trim() === '') { itemDescription = defaultLookupResponse.Items[0].Description || ''; } } else { throw new Error(`Default SKU '${defaultSku}' not found in MYOB`); } } else { throw new Error('No SKU provided and no default SKU available'); } } const lineItem = { Item: { UID: itemUid }, ShipQuantity: item.quantity, UnitPrice: item.unitPrice, }; // Add discount percentage if present (from Shopify discounts) if (item.discountAmount && item.discountAmount > 0 && item.unitPrice > 0) { // Convert discount amount to percentage based on total line value (unit price × quantity) const totalLineValue = item.unitPrice * item.quantity; const discountPercent = (item.discountAmount / totalLineValue) * 100; lineItem.DiscountPercent = Math.round(discountPercent * 100) / 100; // Round to 2 decimal places console.log(`Converted discount amount ${item.discountAmount} to percentage: ${lineItem.DiscountPercent}% (unit price: ${item.unitPrice}, quantity: ${item.quantity}, total line value: ${totalLineValue})`); } // Add TaxCode if provided if (item.taxCodeUid && item.taxCodeUid.trim() !== '') { lineItem.TaxCode = { UID: item.taxCodeUid }; } // Add description if available if (itemDescription && itemDescription.trim() !== '') { lineItem.Description = itemDescription; } lines.push(lineItem); } // Add shipping line item if provided if (shippingSku && shippingSku.trim() !== '') { let shippingItemUid = ''; try { const skuEncoded = encodeURIComponent(`'${shippingSku}'`); const lookupUrl = `/Inventory/Item?$filter=Number eq ${skuEncoded}`; const lookupResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', lookupUrl, {}, {}, baseUrl); if (lookupResponse && lookupResponse.Items && lookupResponse.Items.length > 0) { shippingItemUid = lookupResponse.Items[0].UID; const shippingLineItem = { Item: { UID: shippingItemUid }, ShipQuantity: 1, UnitPrice: shippingPrice, }; // Add default tax code if provided if (defaultTaxCodeUid && defaultTaxCodeUid.trim() !== '') { shippingLineItem.TaxCode = { UID: defaultTaxCodeUid }; } // Add custom shipping description if provided if (shippingDescription && shippingDescription.trim() !== '') { shippingLineItem.Description = shippingDescription; } lines.push(shippingLineItem); } else { throw new Error(`Shipping SKU '${shippingSku}' not found in MYOB`); } } catch (error) { throw new Error(`Failed to lookup Shipping SKU '${shippingSku}': ${error.message}`); } } // Build shipping address from lines (similar to your example) let shippingAddress = ''; const newline = '\r\n'; // Add each line to the shipping address, appending newline after each if (shippingLine1 && shippingLine1.trim() !== '') { shippingAddress = shippingLine1; } if (shippingLine2 && shippingLine2.trim() !== '') { shippingAddress = shippingAddress + (shippingAddress ? newline : '') + shippingLine2; } if (shippingLine3 && shippingLine3.trim() !== '') { shippingAddress = shippingAddress + (shippingAddress ? newline : '') + shippingLine3; } if (shippingLine4 && shippingLine4.trim() !== '') { shippingAddress = shippingAddress + (shippingAddress ? newline : '') + shippingLine4; } if (shippingLine5 && shippingLine5.trim() !== '') { shippingAddress = shippingAddress + (shippingAddress ? newline : '') + shippingLine5; } // Build the payload according to MYOB Business API structure const payload = { OrderType: 'Item', Customer: { UID: customerUid }, Date: new Date().toISOString(), IsTaxInclusive: isTaxInclusive, Lines: lines, }; // Add optional fields if provided if (journalMemo && journalMemo.trim() !== '') { payload.JournalMemo = journalMemo; } if (comment && comment.trim() !== '') { payload.Comment = comment; } if (cleanedPurchaseOrderNumber && cleanedPurchaseOrderNumber.trim() !== '') { payload.CustomerPurchaseOrderNumber = cleanedPurchaseOrderNumber; } if (shippingAddress && shippingAddress.trim() !== '') { payload.ShipToAddress = shippingAddress; } // Step 1: Create the sales order const createResponse = await MyobApi_request_1.myobRequest.call(this, 'POST', '/Sale/Order/Item', payload, {}, baseUrl); // Check if creation was successful let creationSuccessful = false; if (createResponse && typeof createResponse === 'object' && 'statusCode' in createResponse) { creationSuccessful = createResponse.statusCode >= 200 && createResponse.statusCode < 300; } else { // Assume success if we got any response without error creationSuccessful = true; } if (!creationSuccessful) { throw new Error('Failed to create sales order in MYOB'); } // Step 2: Look up the created sales order by purchase order number if (cleanedPurchaseOrderNumber && cleanedPurchaseOrderNumber.trim() !== '') { try { const searchCriteria = `'${cleanedPurchaseOrderNumber}'`; const lookupUrl = `/Sale/Order/Item?$filter=CustomerPurchaseOrderNumber eq ${encodeURIComponent(searchCriteria)}`; const salesOrderResponse = await MyobApi_request_1.myobRequest.call(this, 'GET', lookupUrl, {}, {}, baseUrl); if (salesOrderResponse && salesOrderResponse.Items && salesOrderResponse.Items.length > 0) { // Return the found sales order data returnData.push({ json: salesOrderResponse.Items[0] // Return the first matching sales order }); } else { // Sales order created but not found in lookup (might be timing issue) returnData.push({ json: { success: true, message: 'Sales order created successfully but not found in immediate lookup', purchaseOrderNumber: cleanedPurchaseOrderNumber, note: 'The sales order may take a moment to be available for search. Try searching manually in MYOB.' } }); } } catch (lookupError) { // Sales order created but lookup failed returnData.push({ json: { success: true, message: 'Sales order created successfully but lookup failed', purchaseOrderNumber: cleanedPurchaseOrderNumber, lookupError: lookupError.message, note: 'The sales order was created in MYOB but could not be retrieved immediately.' } }); } } else { // No purchase order number to search by returnData.push({ json: { success: true, message: 'Sales order created successfully', note: 'No purchase order number provided, so the created sales order cannot be retrieved automatically.' } }); } } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error.message || error.toString() } }); } else { throw error; } } return this.prepareOutputData(returnData); } } exports.Myob = Myob;