@fullqueso/mcp-bc-gastos 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bc-client.js +5 -0
- package/package.json +4 -2
- package/server.js +30 -0
- package/tools/payroll/employees.js +147 -0
- package/tools/payroll/index.js +3 -0
- package/tools/payroll/payroll-documents.js +172 -0
- package/tools/payroll/payroll-lines.js +128 -0
- package/tools/reports/manager-report.js +1287 -0
package/lib/bc-client.js
CHANGED
|
@@ -196,6 +196,11 @@ export class BCClient {
|
|
|
196
196
|
return this.buildCustomApiUrl(companyId, 'fullQueso', 'multiPayment', 'v1.0', endpoint, params);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// Shorthand for Payroll API calls (publisher: fullQueso, group: fqPayroll, v1.0)
|
|
200
|
+
buildPayrollApiUrl(companyId, endpoint, params = {}) {
|
|
201
|
+
return this.buildCustomApiUrl(companyId, 'fullQueso', 'fqPayroll', 'v1.0', endpoint, params);
|
|
202
|
+
}
|
|
203
|
+
|
|
199
204
|
// ── OData V4 Web Service Methods ──────────────────────────────────
|
|
200
205
|
// OData endpoints use company NAME (URL-encoded) instead of GUID.
|
|
201
206
|
// Same OAuth token works; different URL path (ODataV4 vs api/v2.0).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullqueso/mcp-bc-gastos",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable,
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable, multi-payment draft visibility, payroll, and manager reports - Full Queso franchise stores",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcp-bc-gastos": "server.js"
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"accounts-receivable",
|
|
39
39
|
"accounts-payable",
|
|
40
40
|
"multi-payment",
|
|
41
|
+
"payroll",
|
|
42
|
+
"manager-report",
|
|
41
43
|
"odata",
|
|
42
44
|
"fullqueso"
|
|
43
45
|
],
|
package/server.js
CHANGED
|
@@ -59,6 +59,16 @@ import {
|
|
|
59
59
|
draftSummaryTool, handleDraftSummary,
|
|
60
60
|
} from './tools/multi-payment/index.js';
|
|
61
61
|
|
|
62
|
+
// Payroll tools
|
|
63
|
+
import {
|
|
64
|
+
payrollDocumentsTool, handlePayrollDocuments,
|
|
65
|
+
payrollLinesTool, handlePayrollLines,
|
|
66
|
+
employeesTool, handleEmployees,
|
|
67
|
+
} from './tools/payroll/index.js';
|
|
68
|
+
|
|
69
|
+
// Reports tools
|
|
70
|
+
import { managerReportTool, handleGenerateManagerReport } from './tools/reports/manager-report.js';
|
|
71
|
+
|
|
62
72
|
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
63
73
|
const bcClient = new BCClient();
|
|
64
74
|
|
|
@@ -108,6 +118,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
108
118
|
draftReceivablesTool,
|
|
109
119
|
draftPayablesTool,
|
|
110
120
|
draftSummaryTool,
|
|
121
|
+
// Payroll
|
|
122
|
+
payrollDocumentsTool,
|
|
123
|
+
payrollLinesTool,
|
|
124
|
+
employeesTool,
|
|
125
|
+
// Reports
|
|
126
|
+
managerReportTool,
|
|
111
127
|
],
|
|
112
128
|
};
|
|
113
129
|
});
|
|
@@ -204,6 +220,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
204
220
|
case 'get_draft_summary':
|
|
205
221
|
result = await handleDraftSummary(bcClient, args);
|
|
206
222
|
break;
|
|
223
|
+
// Payroll
|
|
224
|
+
case 'get_payroll_documents':
|
|
225
|
+
result = await handlePayrollDocuments(bcClient, args);
|
|
226
|
+
break;
|
|
227
|
+
case 'get_payroll_lines':
|
|
228
|
+
result = await handlePayrollLines(bcClient, args);
|
|
229
|
+
break;
|
|
230
|
+
case 'get_employees':
|
|
231
|
+
result = await handleEmployees(bcClient, args);
|
|
232
|
+
break;
|
|
233
|
+
// Reports
|
|
234
|
+
case 'generate_manager_report':
|
|
235
|
+
result = await handleGenerateManagerReport(bcClient, args);
|
|
236
|
+
break;
|
|
207
237
|
default:
|
|
208
238
|
return {
|
|
209
239
|
content: [{ type: 'text', text: `Herramienta desconocida: ${name}` }],
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { resolveStores } from '../../config/company-config.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export const employeesTool = {
|
|
5
|
+
name: 'get_employees',
|
|
6
|
+
description:
|
|
7
|
+
'Lista empleados de una tienda con datos de nomina: tipo, status, salarios base, bonos predeterminados, fechas. Filtros por status, tipo de nomina, busqueda por nombre.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
store: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
|
|
14
|
+
description: 'Tienda(s) a consultar.',
|
|
15
|
+
},
|
|
16
|
+
status: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['Active', 'Inactive'],
|
|
19
|
+
description: "Filtrar por status. Default: 'Active'.",
|
|
20
|
+
},
|
|
21
|
+
payroll_type: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['SEMANAL', 'QUINCENAL', 'MENSUAL'],
|
|
24
|
+
description: 'Filtrar por tipo de nomina.',
|
|
25
|
+
},
|
|
26
|
+
employee_search: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Busqueda parcial por nombre (case-insensitive, client-side).',
|
|
29
|
+
},
|
|
30
|
+
employee_code: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Filtrar por codigo de empleado exacto.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['store'],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function handleEmployees(bcClient, args) {
|
|
40
|
+
const storeParam = args.store;
|
|
41
|
+
if (!storeParam) throw new Error('Parametro requerido: store');
|
|
42
|
+
|
|
43
|
+
const stores = resolveStores(storeParam === 'all' ? null : [storeParam]);
|
|
44
|
+
|
|
45
|
+
if (stores.length === 1) {
|
|
46
|
+
return processStore(bcClient, stores[0], args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const results = await Promise.all(
|
|
50
|
+
stores.map((s) => processStore(bcClient, s, args))
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const consolidated = {
|
|
54
|
+
total_employees: 0,
|
|
55
|
+
by_status: {},
|
|
56
|
+
by_payroll_type: {},
|
|
57
|
+
by_employee_type: {},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
consolidated.total_employees += r.summary.total_employees;
|
|
62
|
+
for (const [k, v] of Object.entries(r.summary.by_status)) {
|
|
63
|
+
consolidated.by_status[k] = (consolidated.by_status[k] || 0) + v;
|
|
64
|
+
}
|
|
65
|
+
for (const [k, v] of Object.entries(r.summary.by_payroll_type)) {
|
|
66
|
+
consolidated.by_payroll_type[k] = (consolidated.by_payroll_type[k] || 0) + v;
|
|
67
|
+
}
|
|
68
|
+
for (const [k, v] of Object.entries(r.summary.by_employee_type)) {
|
|
69
|
+
consolidated.by_employee_type[k] = (consolidated.by_employee_type[k] || 0) + v;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { stores: results, consolidated };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function processStore(bcClient, storeInfo, args) {
|
|
77
|
+
const companyId = storeInfo.companyId;
|
|
78
|
+
const status = args.status || 'Active';
|
|
79
|
+
|
|
80
|
+
const filters = [`companyCode eq '${storeInfo.code}'`, `status eq '${status}'`];
|
|
81
|
+
if (args.payroll_type) filters.push(`payrollType eq '${args.payroll_type}'`);
|
|
82
|
+
if (args.employee_code) filters.push(`employeeCode eq '${args.employee_code}'`);
|
|
83
|
+
|
|
84
|
+
const url = bcClient.buildPayrollApiUrl(companyId, 'fqEmployees', {
|
|
85
|
+
$filter: filters.join(' and '),
|
|
86
|
+
$select: 'employeeCode,employeeName,cedulaDeIdentidad,companyCode,employeeType,status,payrollType,startDate,endDate,defaultTransportBonusVES,bonoAlimentacionUSD,salarioBaseSemanalVES,salarioBaseMensualVES,bonoProduccionUSD',
|
|
87
|
+
$orderby: 'employeeName',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const data = await bcClient.apiCallAllPages(url);
|
|
91
|
+
logger.info(`${storeInfo.code}: ${data.length} employees (status=${status})`);
|
|
92
|
+
|
|
93
|
+
let employees = data.map((emp) => ({
|
|
94
|
+
employee_code: emp.employeeCode,
|
|
95
|
+
employee_name: emp.employeeName,
|
|
96
|
+
cedula: emp.cedulaDeIdentidad || null,
|
|
97
|
+
company_code: emp.companyCode,
|
|
98
|
+
employee_type: emp.employeeType || null,
|
|
99
|
+
status: emp.status,
|
|
100
|
+
payroll_type: emp.payrollType || null,
|
|
101
|
+
start_date: emp.startDate || null,
|
|
102
|
+
end_date: emp.endDate || null,
|
|
103
|
+
salary: {
|
|
104
|
+
base_semanal_ves: round2(emp.salarioBaseSemanalVES || 0),
|
|
105
|
+
base_mensual_ves: round2(emp.salarioBaseMensualVES || 0),
|
|
106
|
+
},
|
|
107
|
+
default_bonuses: {
|
|
108
|
+
transport_ves: round2(emp.defaultTransportBonusVES || 0),
|
|
109
|
+
alimentacion_usd: round2(emp.bonoAlimentacionUSD || 0),
|
|
110
|
+
produccion_usd: round2(emp.bonoProduccionUSD || 0),
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// Client-side name search
|
|
115
|
+
if (args.employee_search) {
|
|
116
|
+
const search = args.employee_search.toLowerCase();
|
|
117
|
+
employees = employees.filter((e) => e.employee_name && e.employee_name.toLowerCase().includes(search));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const summary = {
|
|
121
|
+
total_employees: employees.length,
|
|
122
|
+
by_status: {},
|
|
123
|
+
by_payroll_type: {},
|
|
124
|
+
by_employee_type: {},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const emp of employees) {
|
|
128
|
+
summary.by_status[emp.status] = (summary.by_status[emp.status] || 0) + 1;
|
|
129
|
+
if (emp.payroll_type) {
|
|
130
|
+
summary.by_payroll_type[emp.payroll_type] = (summary.by_payroll_type[emp.payroll_type] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
if (emp.employee_type) {
|
|
133
|
+
summary.by_employee_type[emp.employee_type] = (summary.by_employee_type[emp.employee_type] || 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
store: storeInfo.code,
|
|
139
|
+
store_name: storeInfo.name,
|
|
140
|
+
summary,
|
|
141
|
+
employees,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function round2(n) {
|
|
146
|
+
return Math.round(n * 100) / 100;
|
|
147
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { resolveStores } from '../../config/company-config.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export const payrollDocumentsTool = {
|
|
5
|
+
name: 'get_payroll_documents',
|
|
6
|
+
description:
|
|
7
|
+
'Lista documentos de nomina (headers) con filtros por periodo, tipo, status. Muestra totales por documento, cantidad de empleados, tasas de cambio. Soporta multi-tienda.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
store: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
|
|
14
|
+
description: 'Tienda(s) a consultar.',
|
|
15
|
+
},
|
|
16
|
+
period_code: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: "Filtrar por codigo de periodo exacto (ej: 'FQ88-2025-03-W10').",
|
|
19
|
+
},
|
|
20
|
+
payroll_type: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['SEMANAL', 'QUINCENAL', 'MENSUAL'],
|
|
23
|
+
description: 'Filtrar por tipo de nomina.',
|
|
24
|
+
},
|
|
25
|
+
status: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
enum: ['Draft', 'Posted'],
|
|
28
|
+
description: 'Filtrar por status. Sin filtro = todos.',
|
|
29
|
+
},
|
|
30
|
+
start_date: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Fecha inicio (YYYY-MM-DD) para filtrar por documentDate.',
|
|
33
|
+
},
|
|
34
|
+
end_date: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Fecha fin (YYYY-MM-DD) para filtrar por documentDate.',
|
|
37
|
+
},
|
|
38
|
+
summary_only: {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
description: 'Solo resumen (sin lista de documentos). Default: false.',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['store'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export async function handlePayrollDocuments(bcClient, args) {
|
|
48
|
+
const storeParam = args.store;
|
|
49
|
+
if (!storeParam) throw new Error('Parametro requerido: store');
|
|
50
|
+
|
|
51
|
+
const stores = resolveStores(storeParam === 'all' ? null : [storeParam]);
|
|
52
|
+
const summaryOnly = args.summary_only || false;
|
|
53
|
+
|
|
54
|
+
if (stores.length === 1) {
|
|
55
|
+
return processStore(bcClient, stores[0], args, summaryOnly);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = await Promise.all(
|
|
59
|
+
stores.map((s) => processStore(bcClient, s, args, summaryOnly))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const consolidated = {
|
|
63
|
+
total_documents: 0,
|
|
64
|
+
total_employees_lines: 0,
|
|
65
|
+
total_net_usd: 0,
|
|
66
|
+
total_net_ves: 0,
|
|
67
|
+
by_status: { Draft: 0, Posted: 0 },
|
|
68
|
+
by_type: {},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const r of results) {
|
|
72
|
+
consolidated.total_documents += r.summary.total_documents;
|
|
73
|
+
consolidated.total_employees_lines += r.summary.total_employees_lines;
|
|
74
|
+
consolidated.total_net_usd += r.summary.total_net_usd;
|
|
75
|
+
consolidated.total_net_ves += r.summary.total_net_ves;
|
|
76
|
+
consolidated.by_status.Draft += r.summary.by_status.Draft || 0;
|
|
77
|
+
consolidated.by_status.Posted += r.summary.by_status.Posted || 0;
|
|
78
|
+
for (const [type, count] of Object.entries(r.summary.by_type)) {
|
|
79
|
+
consolidated.by_type[type] = (consolidated.by_type[type] || 0) + count;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
consolidated.total_net_usd = round2(consolidated.total_net_usd);
|
|
84
|
+
consolidated.total_net_ves = round2(consolidated.total_net_ves);
|
|
85
|
+
|
|
86
|
+
return { stores: results, consolidated };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function processStore(bcClient, storeInfo, args, summaryOnly) {
|
|
90
|
+
const companyId = storeInfo.companyId;
|
|
91
|
+
|
|
92
|
+
const filters = [];
|
|
93
|
+
if (args.period_code) filters.push(`periodCode eq '${args.period_code}'`);
|
|
94
|
+
if (args.payroll_type) filters.push(`payrollType eq '${args.payroll_type}'`);
|
|
95
|
+
if (args.status) filters.push(`status eq '${args.status}'`);
|
|
96
|
+
if (args.start_date) filters.push(`documentDate ge ${args.start_date}`);
|
|
97
|
+
if (args.end_date) filters.push(`documentDate le ${args.end_date}`);
|
|
98
|
+
|
|
99
|
+
const params = {
|
|
100
|
+
$select: 'documentNo,periodCode,payrollType,companyCode,documentDate,postingDate,paymentDate,status,lineCount,totalBonusesVES,totalBonusesUSD,totalNetUSD,totalNetVES,exchangeRate,createdBy,postedBy,createdDateTime,postedDateTime',
|
|
101
|
+
$orderby: 'documentDate desc',
|
|
102
|
+
};
|
|
103
|
+
if (filters.length > 0) params.$filter = filters.join(' and ');
|
|
104
|
+
|
|
105
|
+
const url = bcClient.buildPayrollApiUrl(companyId, 'fqPayrollDocuments', params);
|
|
106
|
+
const data = await bcClient.apiCallAllPages(url);
|
|
107
|
+
|
|
108
|
+
logger.info(`${storeInfo.code}: ${data.length} payroll documents`);
|
|
109
|
+
|
|
110
|
+
const summary = {
|
|
111
|
+
total_documents: data.length,
|
|
112
|
+
total_employees_lines: 0,
|
|
113
|
+
total_net_usd: 0,
|
|
114
|
+
total_net_ves: 0,
|
|
115
|
+
by_status: { Draft: 0, Posted: 0 },
|
|
116
|
+
by_type: {},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const documents = [];
|
|
120
|
+
|
|
121
|
+
for (const doc of data) {
|
|
122
|
+
const lineCount = doc.lineCount || 0;
|
|
123
|
+
const netUsd = doc.totalNetUSD || 0;
|
|
124
|
+
const netVes = doc.totalNetVES || 0;
|
|
125
|
+
|
|
126
|
+
summary.total_employees_lines += lineCount;
|
|
127
|
+
summary.total_net_usd += netUsd;
|
|
128
|
+
summary.total_net_ves += netVes;
|
|
129
|
+
summary.by_status[doc.status] = (summary.by_status[doc.status] || 0) + 1;
|
|
130
|
+
summary.by_type[doc.payrollType] = (summary.by_type[doc.payrollType] || 0) + 1;
|
|
131
|
+
|
|
132
|
+
if (!summaryOnly) {
|
|
133
|
+
documents.push({
|
|
134
|
+
document_no: doc.documentNo,
|
|
135
|
+
period_code: doc.periodCode,
|
|
136
|
+
payroll_type: doc.payrollType,
|
|
137
|
+
company_code: doc.companyCode,
|
|
138
|
+
document_date: doc.documentDate,
|
|
139
|
+
posting_date: doc.postingDate,
|
|
140
|
+
payment_date: doc.paymentDate,
|
|
141
|
+
status: doc.status,
|
|
142
|
+
line_count: lineCount,
|
|
143
|
+
total_bonuses_ves: round2(doc.totalBonusesVES || 0),
|
|
144
|
+
total_bonuses_usd: round2(doc.totalBonusesUSD || 0),
|
|
145
|
+
total_net_usd: round2(netUsd),
|
|
146
|
+
total_net_ves: round2(netVes),
|
|
147
|
+
exchange_rate: doc.exchangeRate || null,
|
|
148
|
+
created_by: doc.createdBy || null,
|
|
149
|
+
posted_by: doc.postedBy || null,
|
|
150
|
+
created_date_time: doc.createdDateTime || null,
|
|
151
|
+
posted_date_time: doc.postedDateTime || null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
summary.total_net_usd = round2(summary.total_net_usd);
|
|
157
|
+
summary.total_net_ves = round2(summary.total_net_ves);
|
|
158
|
+
|
|
159
|
+
const result = {
|
|
160
|
+
store: storeInfo.code,
|
|
161
|
+
store_name: storeInfo.name,
|
|
162
|
+
summary,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!summaryOnly) result.documents = documents;
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function round2(n) {
|
|
171
|
+
return Math.round(n * 100) / 100;
|
|
172
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { resolveStores } from '../../config/company-config.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export const payrollLinesTool = {
|
|
5
|
+
name: 'get_payroll_lines',
|
|
6
|
+
description:
|
|
7
|
+
'Detalle de nomina por empleado para un documento especifico. Muestra bonos, pagos por metodo (cash VES, banco VES, cash USD), totales, y balance. Requiere document_no.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
store: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
enum: ['FQ01', 'FQ28', 'FQ88'],
|
|
14
|
+
description: 'Tienda a consultar.',
|
|
15
|
+
},
|
|
16
|
+
document_no: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: "Numero de documento de nomina (ej: 'FQ88-2025-03-W10-0001').",
|
|
19
|
+
},
|
|
20
|
+
employee_code: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Filtrar por codigo de empleado especifico.',
|
|
23
|
+
},
|
|
24
|
+
employee_search: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Busqueda parcial por nombre de empleado (case-insensitive, client-side).',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ['store', 'document_no'],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function handlePayrollLines(bcClient, args) {
|
|
34
|
+
const { store, document_no } = args;
|
|
35
|
+
if (!store) throw new Error('Parametro requerido: store');
|
|
36
|
+
if (!document_no) throw new Error('Parametro requerido: document_no');
|
|
37
|
+
|
|
38
|
+
const stores = resolveStores([store]);
|
|
39
|
+
const storeInfo = stores[0];
|
|
40
|
+
const companyId = storeInfo.companyId;
|
|
41
|
+
|
|
42
|
+
const filters = [`documentNo eq '${document_no}'`];
|
|
43
|
+
if (args.employee_code) filters.push(`employeeCode eq '${args.employee_code}'`);
|
|
44
|
+
|
|
45
|
+
const url = bcClient.buildPayrollApiUrl(companyId, 'fqPayrollLines', {
|
|
46
|
+
$filter: filters.join(' and '),
|
|
47
|
+
$select: 'documentNo,lineNo,employeeCode,employeeName,cedulaDeIdentidad,transportBonusVES,productivityBonusUSD,extraRedobleUSD,specialDateBonusUSD,foodBonusUSD,payCashVES,payBankVES,payCashUSD,totalBonusesVES,totalBonusesUSD,totalPayVES,totalPayUSD,balanceDifference',
|
|
48
|
+
$orderby: 'lineNo',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const data = await bcClient.apiCallAllPages(url);
|
|
52
|
+
logger.info(`${storeInfo.code}: ${data.length} payroll lines for ${document_no}`);
|
|
53
|
+
|
|
54
|
+
let lines = data.map((line) => ({
|
|
55
|
+
document_no: line.documentNo,
|
|
56
|
+
line_no: line.lineNo,
|
|
57
|
+
employee_code: line.employeeCode,
|
|
58
|
+
employee_name: line.employeeName,
|
|
59
|
+
cedula: line.cedulaDeIdentidad || null,
|
|
60
|
+
bonuses: {
|
|
61
|
+
transport_ves: round2(line.transportBonusVES || 0),
|
|
62
|
+
productivity_usd: round2(line.productivityBonusUSD || 0),
|
|
63
|
+
extra_redoble_usd: round2(line.extraRedobleUSD || 0),
|
|
64
|
+
special_date_usd: round2(line.specialDateBonusUSD || 0),
|
|
65
|
+
food_usd: round2(line.foodBonusUSD || 0),
|
|
66
|
+
},
|
|
67
|
+
payments: {
|
|
68
|
+
cash_ves: round2(line.payCashVES || 0),
|
|
69
|
+
bank_ves: round2(line.payBankVES || 0),
|
|
70
|
+
cash_usd: round2(line.payCashUSD || 0),
|
|
71
|
+
},
|
|
72
|
+
total_bonuses_ves: round2(line.totalBonusesVES || 0),
|
|
73
|
+
total_bonuses_usd: round2(line.totalBonusesUSD || 0),
|
|
74
|
+
total_pay_ves: round2(line.totalPayVES || 0),
|
|
75
|
+
total_pay_usd: round2(line.totalPayUSD || 0),
|
|
76
|
+
balance_difference: round2(line.balanceDifference || 0),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// Client-side name search
|
|
80
|
+
if (args.employee_search) {
|
|
81
|
+
const search = args.employee_search.toLowerCase();
|
|
82
|
+
lines = lines.filter((l) => l.employee_name && l.employee_name.toLowerCase().includes(search));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Summary totals
|
|
86
|
+
const summary = {
|
|
87
|
+
document_no,
|
|
88
|
+
total_employees: lines.length,
|
|
89
|
+
total_bonuses_ves: 0,
|
|
90
|
+
total_bonuses_usd: 0,
|
|
91
|
+
total_pay_ves: 0,
|
|
92
|
+
total_pay_usd: 0,
|
|
93
|
+
total_cash_ves: 0,
|
|
94
|
+
total_bank_ves: 0,
|
|
95
|
+
total_cash_usd: 0,
|
|
96
|
+
employees_with_balance_diff: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
summary.total_bonuses_ves += line.total_bonuses_ves;
|
|
101
|
+
summary.total_bonuses_usd += line.total_bonuses_usd;
|
|
102
|
+
summary.total_pay_ves += line.total_pay_ves;
|
|
103
|
+
summary.total_pay_usd += line.total_pay_usd;
|
|
104
|
+
summary.total_cash_ves += line.payments.cash_ves;
|
|
105
|
+
summary.total_bank_ves += line.payments.bank_ves;
|
|
106
|
+
summary.total_cash_usd += line.payments.cash_usd;
|
|
107
|
+
if (Math.abs(line.balance_difference) > 0.01) summary.employees_with_balance_diff++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
summary.total_bonuses_ves = round2(summary.total_bonuses_ves);
|
|
111
|
+
summary.total_bonuses_usd = round2(summary.total_bonuses_usd);
|
|
112
|
+
summary.total_pay_ves = round2(summary.total_pay_ves);
|
|
113
|
+
summary.total_pay_usd = round2(summary.total_pay_usd);
|
|
114
|
+
summary.total_cash_ves = round2(summary.total_cash_ves);
|
|
115
|
+
summary.total_bank_ves = round2(summary.total_bank_ves);
|
|
116
|
+
summary.total_cash_usd = round2(summary.total_cash_usd);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
store,
|
|
120
|
+
store_name: storeInfo.name,
|
|
121
|
+
summary,
|
|
122
|
+
lines,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function round2(n) {
|
|
127
|
+
return Math.round(n * 100) / 100;
|
|
128
|
+
}
|