From fe64194db59282ff00d1e7bd75398371259a4c19 Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:33:33 -0400 Subject: [PATCH] Add erpnext-mcp/src/tools/selling.ts --- erpnext-mcp/src/tools/selling.ts | 238 +++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 erpnext-mcp/src/tools/selling.ts diff --git a/erpnext-mcp/src/tools/selling.ts b/erpnext-mcp/src/tools/selling.ts new file mode 100644 index 0000000..81b3ceb --- /dev/null +++ b/erpnext-mcp/src/tools/selling.ts @@ -0,0 +1,238 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ERPNextClient, truncateResponse } from "../services/erpnext-client.js"; +import { z } from "zod"; + +export function registerSellingTools(server: McpServer, client: ERPNextClient): void { + + // ─── 23. Make Sales Invoice from Sales Order ─────────── + server.registerTool( + "erpnext_make_sales_invoice_from_so", + { + title: "Make Sales Invoice from Sales Order", + description: `Create a Sales Invoice from a submitted Sales Order.`, + inputSchema: z.object({ source_name: z.string().describe("Sales Order name") }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: `Sales Invoice draft created from ${params.source_name}:\n${JSON.stringify(result, null, 2).slice(0, 3000)}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 24. Make Delivery Note from Sales Order ─────────── + server.registerTool( + "erpnext_make_delivery_note_from_so", + { + title: "Make Delivery Note from Sales Order", + description: `Create a Delivery Note from a submitted Sales Order.`, + inputSchema: z.object({ source_name: z.string().describe("Sales Order name") }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: `Delivery Note draft created from ${params.source_name}:\n${JSON.stringify(result, null, 2).slice(0, 3000)}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 25. Make Material Request from Sales Order ──────── + server.registerTool( + "erpnext_make_material_request_from_so", + { + title: "Make Material Request from Sales Order", + description: `Create a Material Request from a Sales Order.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.doctype.sales_order.sales_order.make_material_request", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 26. Close/Reopen Sales Order ────────────────────── + server.registerTool( + "erpnext_update_sales_order_status", + { + title: "Close/Reopen Sales Order", + description: `Close or reopen a Sales Order.`, + inputSchema: z.object({ + name: z.string(), + status: z.enum(["Closed", "Re-open"]), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("erpnext.selling.doctype.sales_order.sales_order.update_status", { + status: params.status === "Re-open" ? "Draft" : params.status, + name: params.name, + }); + return { content: [{ type: "text", text: `Sales Order ${params.name} ${params.status === "Closed" ? "closed" : "reopened"}.` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 27. Make Quotation ──────────────────────────────── + server.registerTool( + "erpnext_make_quotation", + { + title: "Create Quotation", + description: `Create a Quotation for a customer. + +Args: + - party_name: Customer or Lead name + - company: Company name + - items: Array of {item_code, qty, rate?, warehouse?} + - quotation_to: "Customer" or "Lead" (default "Customer")`, + inputSchema: z.object({ + party_name: z.string(), + company: z.string(), + items: z.array(z.object({ + item_code: z.string(), + qty: z.number(), + rate: z.number().optional(), + warehouse: z.string().optional(), + })), + quotation_to: z.enum(["Customer", "Lead"]).default("Customer"), + transaction_date: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Quotation", { + quotation_to: params.quotation_to, + party_name: params.party_name, + company: params.company, + transaction_date: params.transaction_date, + items: params.items, + }); + return { content: [{ type: "text", text: `Created Quotation: ${doc.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 28. Make Sales Order from Quotation ─────────────── + server.registerTool( + "erpnext_make_sales_order_from_quotation", + { + title: "Make Sales Order from Quotation", + description: `Create a Sales Order from a submitted Quotation.`, + inputSchema: z.object({ source_name: z.string().describe("Quotation name") }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.doctype.quotation.quotation.make_sales_order", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 29. Sales Funnel Data ───────────────────────────── + server.registerTool( + "erpnext_get_sales_funnel", + { + title: "Get Sales Funnel Data", + description: `Get sales funnel data showing opportunity pipeline.`, + inputSchema: z.object({ + from_date: z.string(), + to_date: z.string(), + company: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", + params + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 30. Get Item Details for Transaction ────────────── + server.registerTool( + "erpnext_get_item_details", + { + title: "Get Item Details for Transaction", + description: `Get item details including pricing, tax, and availability for a transaction context. + +Args: + - item_code: Item code + - company: Company name + - doctype: Parent doctype (e.g. "Sales Order", "Purchase Order") + - customer: Customer name (for selling) + - supplier: Supplier name (for buying) + - price_list: Price list name + - warehouse: Warehouse name`, + inputSchema: z.object({ + item_code: z.string(), + company: z.string(), + doctype: z.string().default("Sales Order"), + customer: z.string().optional(), + supplier: z.string().optional(), + price_list: z.string().optional(), + warehouse: z.string().optional(), + currency: z.string().optional(), + conversion_rate: z.number().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.get_item_details.get_item_details", { + args: { + item_code: params.item_code, + company: params.company, + doctype: params.doctype, + customer: params.customer, + supplier: params.supplier, + selling_price_list: params.price_list, + buying_price_list: params.price_list, + warehouse: params.warehouse, + currency: params.currency, + conversion_rate: params.conversion_rate, + }, + }); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); +}