arcadia-suite-sv/server/erpnext/service.ts

306 lines
8.5 KiB
TypeScript

import fetch from "node-fetch";
const ERPNEXT_URL = process.env.ERPNEXT_URL || "";
const ERPNEXT_API_KEY = process.env.ERPNEXT_API_KEY || "";
const ERPNEXT_API_SECRET = process.env.ERPNEXT_API_SECRET || "";
interface ERPNextResponse<T = unknown> {
data: T;
message?: string;
}
interface DocTypeField {
name: string;
label?: string;
fieldtype: string;
options?: string;
reqd?: number;
}
interface DocTypeInfo {
name: string;
module?: string;
is_submittable?: number;
is_tree?: number;
istable?: number;
}
function getAuthHeader(): Record<string, string> {
return {
"Authorization": `token ${ERPNEXT_API_KEY}:${ERPNEXT_API_SECRET}`,
"Content-Type": "application/json",
};
}
function encodeDocType(doctype: string): string {
return encodeURIComponent(doctype);
}
export async function testConnection(): Promise<{ success: boolean; message: string; user?: string }> {
try {
const response = await fetch(`${ERPNEXT_URL}/api/method/frappe.auth.get_logged_user`, {
method: "GET",
headers: getAuthHeader(),
});
if (!response.ok) {
return { success: false, message: `Erro de conexão: ${response.status} ${response.statusText}` };
}
const data = await response.json() as ERPNextResponse<string>;
return { success: true, message: "Conexão estabelecida com sucesso", user: data.message };
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Erro desconhecido";
return { success: false, message: `Erro ao conectar: ${message}` };
}
}
export async function listDocTypes(limit: number = 100): Promise<DocTypeInfo[]> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/DocType?fields=["name","module","is_submittable","is_tree","istable"]&limit_page_length=${limit}`,
{
method: "GET",
headers: getAuthHeader(),
}
);
if (!response.ok) {
throw new Error(`Erro ao listar DocTypes: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<DocTypeInfo[]>;
return data.data;
}
export async function getDocTypeFields(doctype: string): Promise<DocTypeField[]> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/DocType/${encodeDocType(doctype)}`,
{
method: "GET",
headers: getAuthHeader(),
}
);
if (!response.ok) {
throw new Error(`Erro ao buscar campos do DocType ${doctype}: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<{ fields: DocTypeField[] }>;
return data.data.fields || [];
}
export async function getDocuments<T = unknown>(
doctype: string,
filters?: Record<string, unknown>,
fields?: string[],
limit: number = 20,
orderBy?: string
): Promise<T[]> {
let url = `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}?limit_page_length=${limit}`;
if (fields && fields.length > 0) {
url += `&fields=${encodeURIComponent(JSON.stringify(fields))}`;
}
if (filters && Object.keys(filters).length > 0) {
url += `&filters=${encodeURIComponent(JSON.stringify(filters))}`;
}
if (orderBy) {
url += `&order_by=${encodeURIComponent(orderBy)}`;
}
const response = await fetch(url, {
method: "GET",
headers: getAuthHeader(),
});
if (!response.ok) {
throw new Error(`Erro ao buscar documentos de ${doctype}: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<T[]>;
return data.data;
}
export async function getDocument<T = unknown>(doctype: string, name: string): Promise<T> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`,
{
method: "GET",
headers: getAuthHeader(),
}
);
if (!response.ok) {
throw new Error(`Erro ao buscar documento ${doctype}/${name}: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<T>;
return data.data;
}
export async function createDocument<T = unknown>(
doctype: string,
documentData: Record<string, unknown>
): Promise<T> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}`,
{
method: "POST",
headers: getAuthHeader(),
body: JSON.stringify(documentData),
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro ao criar documento ${doctype}: ${response.status} - ${errorText}`);
}
const data = await response.json() as ERPNextResponse<T>;
return data.data;
}
export async function updateDocument<T = unknown>(
doctype: string,
name: string,
documentData: Record<string, unknown>
): Promise<T> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`,
{
method: "PUT",
headers: getAuthHeader(),
body: JSON.stringify(documentData),
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro ao atualizar documento ${doctype}/${name}: ${response.status} - ${errorText}`);
}
const data = await response.json() as ERPNextResponse<T>;
return data.data;
}
export async function deleteDocument(doctype: string, name: string): Promise<{ success: boolean; message: string }> {
const response = await fetch(
`${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`,
{
method: "DELETE",
headers: getAuthHeader(),
}
);
if (!response.ok) {
throw new Error(`Erro ao deletar documento ${doctype}/${name}: ${response.status}`);
}
return { success: true, message: `Documento ${name} deletado com sucesso` };
}
export async function runReport(
reportName: string,
filters?: Record<string, unknown>
): Promise<{ columns: unknown[]; data: unknown[] }> {
let url = `${ERPNEXT_URL}/api/method/frappe.desk.query_report.run?report_name=${encodeURIComponent(reportName)}`;
if (filters && Object.keys(filters).length > 0) {
url += `&filters=${encodeURIComponent(JSON.stringify(filters))}`;
}
const response = await fetch(url, {
method: "GET",
headers: getAuthHeader(),
});
if (!response.ok) {
throw new Error(`Erro ao executar relatório ${reportName}: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<{ result: unknown[]; columns: unknown[] }>;
return {
columns: data.message ? (data as unknown as { message: { columns: unknown[] } }).message.columns : [],
data: data.message ? (data as unknown as { message: { result: unknown[] } }).message.result : [],
};
}
export async function searchDocuments(
doctype: string,
searchTerm: string,
fields?: string[],
limit: number = 20
): Promise<unknown[]> {
const searchFilters: (string | unknown)[][] = [];
const doctypeFields = await getDocTypeFields(doctype);
const textFields = doctypeFields
.filter(f => ["Data", "Text", "Small Text", "Long Text", "Text Editor"].includes(f.fieldtype))
.slice(0, 3)
.map(f => f.name);
if (textFields.length > 0) {
textFields.forEach(field => {
searchFilters.push([doctype, field, "like", `%${searchTerm}%`]);
});
}
const orFilters: (string | unknown)[][] = searchFilters.length > 0
? searchFilters
: [[doctype, "name", "like", `%${searchTerm}%`]];
let url = `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}?limit_page_length=${limit}`;
url += `&or_filters=${encodeURIComponent(JSON.stringify(orFilters))}`;
if (fields && fields.length > 0) {
url += `&fields=${encodeURIComponent(JSON.stringify(fields))}`;
}
const response = await fetch(url, {
method: "GET",
headers: getAuthHeader(),
});
if (!response.ok) {
throw new Error(`Erro na busca em ${doctype}: ${response.status}`);
}
const data = await response.json() as ERPNextResponse<unknown[]>;
return data.data;
}
export async function callMethod<T = unknown>(
method: string,
args?: Record<string, unknown>
): Promise<T> {
const response = await fetch(
`${ERPNEXT_URL}/api/method/${method}`,
{
method: "POST",
headers: getAuthHeader(),
body: args ? JSON.stringify(args) : undefined,
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro ao chamar método ${method}: ${response.status} - ${errorText}`);
}
const data = await response.json() as ERPNextResponse<T>;
return data.message as T || data.data;
}
export function isConfigured(): boolean {
return Boolean(ERPNEXT_URL && ERPNEXT_API_KEY && ERPNEXT_API_SECRET);
}
export function getConfig(): { url: string; configured: boolean } {
return {
url: ERPNEXT_URL,
configured: isConfigured(),
};
}