sap-b1-mcp-server
Version:
SAP Business One Service Layer MCP Server
646 lines • 26.5 kB
JavaScript
import { SessionManager } from './session.js';
export class SAPClient {
sessionManager;
constructor(config) {
this.sessionManager = SessionManager.getInstance(config);
}
/**
* Login to SAP B1
*/
async login() {
return await this.sessionManager.login();
}
/**
* Logout from SAP B1
*/
async logout() {
await this.sessionManager.logout();
}
/**
* Check session status
*/
async getSessionStatus() {
const valid = await this.sessionManager.isSessionValid();
const sessionInfo = this.sessionManager.getSessionInfo();
return { valid, sessionInfo };
}
/**
* Get business partners with optional filters
*/
async getBusinessPartners(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/BusinessPartners${queryString}`);
return response.value;
}
/**
* Get business partners with full OData response including pagination info
*/
async getBusinessPartnersWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/BusinessPartners${queryString}`);
return response;
}
/**
* Get a single business partner by code
*/
async getBusinessPartner(cardCode, params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/BusinessPartners('${encodeURIComponent(cardCode)}')${queryString}`);
return response;
}
/**
* Create a new business partner
*/
async createBusinessPartner(businessPartner) {
const response = await this.sessionManager.executeRequest('POST', '/b1s/v1/BusinessPartners', businessPartner);
return response;
}
/**
* Update an existing business partner
*/
async updateBusinessPartner(cardCode, updates) {
await this.sessionManager.executeRequest('PATCH', `/b1s/v1/BusinessPartners('${encodeURIComponent(cardCode)}')`, updates);
}
/**
* Get items with optional filters
*/
async getItems(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items${queryString}`);
return response.value;
}
/**
* Get items with full OData response including pagination info
*/
async getItemsWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items${queryString}`);
return response;
}
/**
* Get item groups (product categories) with optional filters
*/
async getItemGroups(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemGroups${queryString}`);
return response.value;
}
/**
* Get item groups with full OData response including pagination info
*/
async getItemGroupsWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemGroups${queryString}`);
return response;
}
/**
* Get a single item by code
*/
async getItem(itemCode, params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items('${encodeURIComponent(itemCode)}')${queryString}`);
return response;
}
/**
* Create a new item
*/
async createItem(item) {
const response = await this.sessionManager.executeRequest('POST', '/b1s/v1/Items', item);
return response;
}
/**
* Update an existing item
*/
async updateItem(itemCode, updates) {
await this.sessionManager.executeRequest('PATCH', `/b1s/v1/Items('${encodeURIComponent(itemCode)}')`, updates);
}
/**
* Get sales orders with optional filters
*/
async getSalesOrders(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Orders${queryString}`);
return response.value;
}
/**
* Get sales orders with full OData response including pagination info
*/
async getSalesOrdersWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Orders${queryString}`);
return response;
}
/**
* Get a single sales order by DocEntry or DocNum
*/
async getSalesOrder(identifier, params) {
const queryString = this.buildQueryString(params);
if (identifier.docEntry) {
// Use direct DocEntry access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Orders(${identifier.docEntry})${queryString}`);
return response;
}
else if (identifier.docNum) {
// Use filter by DocNum
const filterParams = {
...params,
$filter: `DocNum eq ${identifier.docNum}`,
$top: 1
};
const queryStringWithFilter = this.buildQueryString(filterParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Orders${queryStringWithFilter}`);
if (response.value.length === 0) {
throw new Error(`Sales order with DocNum ${identifier.docNum} not found`);
}
return response.value[0];
}
else {
throw new Error('Either docEntry or docNum must be provided');
}
}
/**
* Create a new sales order
*/
async createSalesOrder(order) {
const response = await this.sessionManager.executeRequest('POST', '/b1s/v1/Orders', order);
return response;
}
/**
* Update an existing sales order
*/
async updateSalesOrder(docEntry, updates) {
await this.sessionManager.executeRequest('PATCH', `/b1s/v1/Orders(${docEntry})`, updates);
}
/**
* Get purchase orders with optional filters
*/
async getPurchaseOrders(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseOrders${queryString}`);
return response.value;
}
/**
* Get purchase orders with full OData response including pagination info
*/
async getPurchaseOrdersWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseOrders${queryString}`);
return response;
}
/**
* Get a single purchase order by DocEntry or DocNum
*/
async getPurchaseOrder(identifier, params) {
const queryString = this.buildQueryString(params);
if (identifier.docEntry) {
// Use direct DocEntry access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseOrders(${identifier.docEntry})${queryString}`);
return response;
}
else if (identifier.docNum) {
// Use filter by DocNum
const filterParams = {
...params,
$filter: `DocNum eq ${identifier.docNum}`,
$top: 1
};
const queryStringWithFilter = this.buildQueryString(filterParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseOrders${queryStringWithFilter}`);
if (response.value.length === 0) {
throw new Error(`Purchase order with DocNum ${identifier.docNum} not found`);
}
return response.value[0];
}
else {
throw new Error('Either docEntry or docNum must be provided');
}
}
/**
* Create a new purchase order
*/
async createPurchaseOrder(order) {
const response = await this.sessionManager.executeRequest('POST', '/b1s/v1/PurchaseOrders', order);
return response;
}
/**
* Update an existing purchase order
*/
async updatePurchaseOrder(docEntry, updates) {
await this.sessionManager.executeRequest('PATCH', `/b1s/v1/PurchaseOrders(${docEntry})`, updates);
}
/**
* Get goods receipts with optional filters
*/
async getGoodsReceipts(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseDeliveryNotes${queryString}`);
return response.value;
}
/**
* Get goods receipts with full OData response including pagination info
*/
async getGoodsReceiptsWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseDeliveryNotes${queryString}`);
return response;
}
/**
* Get a single goods receipt by DocEntry or DocNum
*/
async getGoodsReceipt(identifier, params) {
const queryString = this.buildQueryString(params);
if (identifier.docEntry) {
// Use direct DocEntry access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseDeliveryNotes(${identifier.docEntry})${queryString}`);
return response;
}
else if (identifier.docNum) {
// Use filter by DocNum
const filterParams = {
...params,
$filter: `DocNum eq ${identifier.docNum}`,
$top: 1
};
const queryStringWithFilter = this.buildQueryString(filterParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PurchaseDeliveryNotes${queryStringWithFilter}`);
if (response.value.length === 0) {
throw new Error(`Goods receipt with DocNum ${identifier.docNum} not found`);
}
return response.value[0];
}
else {
throw new Error('Either docEntry or docNum must be provided');
}
}
/**
* Get invoices with optional filters
*/
async getInvoices(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Invoices${queryString}`);
return response.value;
}
/**
* Get invoices with full OData response including pagination info
*/
async getInvoicesWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Invoices${queryString}`);
return response;
}
/**
* Get a single invoice by DocEntry or DocNum
*/
async getInvoice(identifier, params) {
const queryString = this.buildQueryString(params);
if (identifier.docEntry) {
// Use direct DocEntry access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Invoices(${identifier.docEntry})${queryString}`);
return response;
}
else if (identifier.docNum) {
// Use filter by DocNum
const filterParams = {
...params,
$filter: `DocNum eq ${identifier.docNum}`,
$top: 1
};
const queryStringWithFilter = this.buildQueryString(filterParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Invoices${queryStringWithFilter}`);
if (response.value.length === 0) {
throw new Error(`Invoice with DocNum ${identifier.docNum} not found`);
}
return response.value[0];
}
else {
throw new Error('Either docEntry or docNum must be provided');
}
}
/**
* Create a new invoice
*/
async createInvoice(invoice) {
const response = await this.sessionManager.executeRequest('POST', '/b1s/v1/Invoices', invoice);
return response;
}
/**
* Update an existing invoice
*/
async updateInvoice(docEntry, updates) {
await this.sessionManager.executeRequest('PATCH', `/b1s/v1/Invoices(${docEntry})`, updates);
}
/**
* Execute a custom OData query
*/
async executeQuery(entityType, params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/${entityType}${queryString}`);
return response.value;
}
/**
* Execute a cross-join query for complex data relationships
*/
async executeCrossJoinQuery(query) {
// Cross-join queries in SAP B1 typically use the $crossjoin system query option
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/$crossjoin(${encodeURIComponent(query)})`);
return response.value;
}
/**
* Test connection to SAP B1
*/
async testConnection() {
try {
const sessionInfo = await this.sessionManager.ensureValidSession();
return {
success: true,
message: 'Successfully connected to SAP Business One',
sessionInfo
};
}
catch (error) {
return {
success: false,
message: error.message || 'Failed to connect to SAP Business One'
};
}
}
/**
* Build query string from parameters
*/
buildQueryString(params) {
// Set default pagination to overcome SAP B1 Service Layer 20-record default limit
const defaultParams = {
$top: 100, // Default to 100 records instead of SAP's 20-record default
...params
};
const queryParams = [];
if (defaultParams.$filter) {
queryParams.push(`$filter=${encodeURIComponent(defaultParams.$filter)}`);
}
if (defaultParams.$select) {
queryParams.push(`$select=${encodeURIComponent(defaultParams.$select)}`);
}
if (defaultParams.$expand) {
queryParams.push(`$expand=${encodeURIComponent(defaultParams.$expand)}`);
}
if (defaultParams.$orderby) {
queryParams.push(`$orderby=${encodeURIComponent(defaultParams.$orderby)}`);
}
if (defaultParams.$top !== undefined) {
queryParams.push(`$top=${defaultParams.$top}`);
}
if (defaultParams.$skip !== undefined) {
queryParams.push(`$skip=${defaultParams.$skip}`);
}
if (defaultParams.$count) {
queryParams.push(`$count=${defaultParams.$count}`);
}
return queryParams.length > 0 ? `?${queryParams.join('&')}` : '?$top=100';
}
/**
* Get warehouses with optional filters
*/
async getWarehouses(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Warehouses${queryString}`);
return response.value;
}
/**
* Get warehouses with full OData response including pagination info
*/
async getWarehousesWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Warehouses${queryString}`);
return response;
}
/**
* Get a single warehouse by WarehouseCode
*/
async getWarehouse(warehouseCode, params) {
const queryString = this.buildQueryString(params);
if (!warehouseCode) {
throw new Error('WarehouseCode is required');
}
// Use direct WarehouseCode access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Warehouses('${warehouseCode}')${queryString}`);
return response;
}
/**
* Get price lists with optional filters
*/
async getPriceLists(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PriceLists${queryString}`);
return response.value;
}
/**
* Get price lists with full OData response including pagination info
*/
async getPriceListsWithPagination(params) {
const queryString = this.buildQueryString(params);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PriceLists${queryString}`);
return response;
}
/**
* Get a single price list by PriceListNo
*/
async getPriceList(priceListNo, params) {
const queryString = this.buildQueryString(params);
if (!priceListNo) {
throw new Error('PriceListNo is required');
}
// Use direct PriceListNo access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/PriceLists(${priceListNo})${queryString}`);
return response;
}
/**
* Get item stock levels for a specific item and warehouse
*/
async getItemStock(itemCode, warehouseCode, params) {
if (!itemCode) {
throw new Error('ItemCode is required');
}
if (!warehouseCode) {
throw new Error('WarehouseCode is required');
}
// Try alternative approach using ItemWarehouseInfoCollection entity directly
const filterParams = {
...params,
$filter: `ItemCode eq '${itemCode}' and WarehouseCode eq '${warehouseCode}'`,
$top: 1
};
const queryString = this.buildQueryString(filterParams);
try {
// First try direct entity access
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemWarehouseInfoCollection${queryString}`);
if (response.value.length === 0) {
throw new Error(`No stock information found for item ${itemCode} in warehouse ${warehouseCode}`);
}
return response.value[0];
}
catch (error) {
// Fallback: create simplified stock info from Items entity
const itemResponse = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items('${itemCode}')`);
// Return basic stock information from Items entity
return {
ItemCode: itemCode,
WarehouseCode: warehouseCode,
InStock: itemResponse.QuantityOnStock || 0,
Available: itemResponse.QuantityOnStock || 0,
Committed: 0,
Ordered: itemResponse.QuantityOrderedFromVendors || 0
};
}
}
/**
* Get all stock levels for a specific item across all warehouses
*/
async getItemStockAll(itemCode, params) {
if (!itemCode) {
throw new Error('ItemCode is required');
}
// Try alternative approach using ItemWarehouseInfoCollection entity directly
const filterParams = {
...params,
$filter: params?.$filter ? `ItemCode eq '${itemCode}' and (${params.$filter})` : `ItemCode eq '${itemCode}'`
};
const queryString = this.buildQueryString(filterParams);
try {
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemWarehouseInfoCollection${queryString}`);
return response.value;
}
catch (error) {
// Fallback: create stock info from Items entity and all warehouses
const itemResponse = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items('${itemCode}')`);
const warehousesResponse = await this.sessionManager.executeRequest('GET', '/b1s/v1/Warehouses?$select=WarehouseCode,WarehouseName');
// Return simplified stock information for each warehouse
return warehousesResponse.value.map(warehouse => ({
ItemCode: itemCode,
WarehouseCode: warehouse.WarehouseCode,
WarehouseName: warehouse.WarehouseName,
InStock: itemResponse.QuantityOnStock || 0, // This is total, not per warehouse
Available: itemResponse.QuantityOnStock || 0,
Committed: 0,
Ordered: itemResponse.QuantityOrderedFromVendors || 0
}));
}
}
/**
* Get item price for a specific item and price list
*/
async getItemPrice(itemCode, priceListId, params) {
if (!itemCode) {
throw new Error('ItemCode is required');
}
if (!priceListId && priceListId !== 0) {
throw new Error('PriceListId is required');
}
// Approach 1: Use the same method as getItem - get the item with price expansion
try {
// Get the item with all price information - this matches how sap_get_item works
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items('${encodeURIComponent(itemCode)}')`);
// SAP B1 items often include price information directly or through navigation properties
// Check if the item has price information for the requested price list
if (response) {
// If ItemPrices navigation property exists
if (response.ItemPrices && Array.isArray(response.ItemPrices)) {
const priceForList = response.ItemPrices.find((p) => p.PriceList === priceListId);
if (priceForList) {
return {
ItemCode: itemCode,
PriceList: priceListId,
Price: priceForList.Price || 0,
Currency: priceForList.Currency || 'USD',
Factor: priceForList.Factor,
PriceListName: priceForList.PriceListName
};
}
}
// If price is embedded directly in the item (for default price list)
if (priceListId === 1 && (response.Price !== undefined || response.UnitPrice !== undefined)) {
return {
ItemCode: itemCode,
PriceList: 1,
Price: response.Price || response.UnitPrice || 0,
Currency: response.Currency || 'USD'
};
}
}
}
catch (error) {
// Continue to other approaches
}
// Approach 2: Try using ItemPrices entity directly
try {
const filterParams = {
...params,
$filter: `ItemCode eq '${itemCode}' and PriceList eq ${priceListId}`,
$top: 1
};
const queryString = this.buildQueryString(filterParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemPrices${queryString}`);
if (response.value.length > 0) {
return response.value[0];
}
}
catch (error) {
// Continue to fallback approach
}
// Approach 3: Try getting the item with explicit ItemPrices expansion
try {
const expandParams = {
$expand: `ItemPrices($filter=PriceList eq ${priceListId})`,
$select: 'ItemCode'
};
const queryString = this.buildQueryString(expandParams);
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/Items('${itemCode}')${queryString}`);
if (response.ItemPrices && response.ItemPrices.length > 0) {
const itemPrice = response.ItemPrices[0];
return {
ItemCode: itemCode,
PriceList: priceListId,
Price: itemPrice.Price || 0,
Currency: itemPrice.Currency || 'USD',
Factor: itemPrice.Factor,
PriceListName: itemPrice.PriceListName
};
}
}
catch (error) {
// Continue to final fallback
}
// Final fallback: throw error if no price found
throw new Error(`No price found for item ${itemCode} in price list ${priceListId}. Item exists but no price is configured for this price list. Available approaches tried: direct item access, ItemPrices entity, and item expansion.`);
}
/**
* Get all prices for a specific item across all price lists
*/
async getItemPricesAll(itemCode, params) {
if (!itemCode) {
throw new Error('ItemCode is required');
}
// Try alternative approach using ItemPrices entity directly
const filterParams = {
...params,
$filter: params?.$filter ? `ItemCode eq '${itemCode}' and (${params.$filter})` : `ItemCode eq '${itemCode}'`
};
const queryString = this.buildQueryString(filterParams);
try {
const response = await this.sessionManager.executeRequest('GET', `/b1s/v1/ItemPrices${queryString}`);
return response.value;
}
catch (error) {
// Fallback: Return basic price information for all price lists
const priceListsResponse = await this.sessionManager.executeRequest('GET', '/b1s/v1/PriceLists?$select=PriceListNo,PriceListName');
// Return basic price information for each price list
return priceListsResponse.value.map(priceList => ({
ItemCode: itemCode,
PriceList: priceList.PriceListNo,
Price: 0, // Would need proper price lookup
Currency: 'USD',
PriceListName: priceList.PriceListName
}));
}
}
}
//# sourceMappingURL=client.js.map