@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 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.11.1",
4
- "description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable, and multi-payment draft visibility - Full Queso franchise stores",
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,3 @@
1
+ export { payrollDocumentsTool, handlePayrollDocuments } from './payroll-documents.js';
2
+ export { payrollLinesTool, handlePayrollLines } from './payroll-lines.js';
3
+ export { employeesTool, handleEmployees } from './employees.js';
@@ -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
+ }