@fullqueso/mcp-bc-gastos 1.28.0 → 1.33.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/.env.example +7 -0
- package/CHANGELOG.md +127 -0
- package/README.md +219 -66
- package/config/bank-gl-map.json +4 -2
- package/config/income-accounts.js +29 -0
- package/lib/bc-client.js +37 -3
- package/package.json +1 -1
- package/scripts/generate-tools-doc.mjs +129 -0
- package/server.js +17 -2
- package/tools/financials/income-statement.js +265 -0
- package/tools/financials/index.js +1 -0
- package/tools/get-crm-rate.js +109 -0
- package/tools/multi-payment/index.js +1 -0
- package/tools/multi-payment/unposted-invoices.js +224 -0
- package/tools/payroll/cost-model.js +51 -0
- package/tools/payroll/employees.js +29 -5
- package/tools/payroll/payroll-documents.js +196 -12
- package/tools/payroll/payroll-lines.js +57 -33
- package/tools/ventas/sales-analysis.js +1 -1
- package/tools/ventas/sales-store-comparison.js +5 -1
- package/utils/sales-aggregation.js +35 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveStores } from '../../config/company-config.js';
|
|
2
2
|
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { round2 } from './cost-model.js';
|
|
3
4
|
|
|
4
5
|
export const payrollLinesTool = {
|
|
5
6
|
name: 'get_payroll_lines',
|
|
6
7
|
description:
|
|
7
|
-
'Detalle de nomina por empleado para un documento
|
|
8
|
+
'Detalle de nomina por empleado para un documento. Muestra bonos, pagos por metodo (cash VES, banco VES, cash USD), y el costo por empleado en USD (cost_usd, derivado de totalVESEquivalent a la tasa del documento) → base para "costo por persona". Requiere document_no. Nota: la API de nomina NO expone dias/horas trabajados ni FTE por linea.',
|
|
8
9
|
inputSchema: {
|
|
9
10
|
type: 'object',
|
|
10
11
|
properties: {
|
|
@@ -42,39 +43,57 @@ export async function handlePayrollLines(bcClient, args) {
|
|
|
42
43
|
const filters = [`documentNo eq '${document_no}'`];
|
|
43
44
|
if (args.employee_code) filters.push(`employeeCode eq '${args.employee_code}'`);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
// Header holds the exchangeRate (lines don't) → needed for per-employee cost_usd.
|
|
47
|
+
const headerUrl = bcClient.buildPayrollApiUrl(companyId, 'fqPayrollDocuments', {
|
|
48
|
+
$filter: `documentNo eq '${document_no}'`,
|
|
49
|
+
$select: 'documentNo,payrollType,status,documentDate,exchangeRate',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const linesUrl = bcClient.buildPayrollApiUrl(companyId, 'fqPayrollLines', {
|
|
46
53
|
$filter: filters.join(' and '),
|
|
47
|
-
$select:
|
|
54
|
+
$select:
|
|
55
|
+
'documentNo,lineNo,employeeCode,employeeName,cedulaDeIdentidad,transportBonusVES,productivityBonusUSD,extraRedobleUSD,specialDateBonusUSD,foodBonusUSD,payCashVES,payBankVES,payCashUSD,totalBonusesVES,totalBonusesUSD,totalVESEquivalent,totalPayVES,totalPayUSD,balanceDifference',
|
|
48
56
|
$orderby: 'lineNo',
|
|
49
57
|
});
|
|
50
58
|
|
|
51
|
-
const data = await
|
|
52
|
-
|
|
59
|
+
const [headerRows, data] = await Promise.all([
|
|
60
|
+
bcClient.apiCallAllPages(headerUrl),
|
|
61
|
+
bcClient.apiCallAllPages(linesUrl),
|
|
62
|
+
]);
|
|
63
|
+
const header = headerRows[0] || null;
|
|
64
|
+
const rate = header?.exchangeRate || 0;
|
|
65
|
+
logger.info(`${storeInfo.code}: ${data.length} payroll lines for ${document_no} (rate=${rate || 'n/a'})`);
|
|
53
66
|
|
|
54
|
-
let lines = data.map((line) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
let lines = data.map((line) => {
|
|
68
|
+
const vesEquivalent = round2(line.totalVESEquivalent || 0);
|
|
69
|
+
return {
|
|
70
|
+
document_no: line.documentNo,
|
|
71
|
+
line_no: line.lineNo,
|
|
72
|
+
employee_code: line.employeeCode,
|
|
73
|
+
employee_name: line.employeeName,
|
|
74
|
+
cedula: line.cedulaDeIdentidad || null,
|
|
75
|
+
bonuses: {
|
|
76
|
+
transport_ves: round2(line.transportBonusVES || 0),
|
|
77
|
+
productivity_usd: round2(line.productivityBonusUSD || 0),
|
|
78
|
+
extra_redoble_usd: round2(line.extraRedobleUSD || 0),
|
|
79
|
+
special_date_usd: round2(line.specialDateBonusUSD || 0),
|
|
80
|
+
food_usd: round2(line.foodBonusUSD || 0),
|
|
81
|
+
},
|
|
82
|
+
payments: {
|
|
83
|
+
cash_ves: round2(line.payCashVES || 0),
|
|
84
|
+
bank_ves: round2(line.payBankVES || 0),
|
|
85
|
+
cash_usd: round2(line.payCashUSD || 0),
|
|
86
|
+
},
|
|
87
|
+
total_bonuses_ves: round2(line.totalBonusesVES || 0),
|
|
88
|
+
total_bonuses_usd: round2(line.totalBonusesUSD || 0),
|
|
89
|
+
total_ves_equivalent: vesEquivalent,
|
|
90
|
+
// Per-employee cost in USD = full pay (VES-equivalent) at the document rate.
|
|
91
|
+
cost_usd: rate ? round2(vesEquivalent / rate) : null,
|
|
92
|
+
total_pay_ves: round2(line.totalPayVES || 0),
|
|
93
|
+
total_pay_usd: round2(line.totalPayUSD || 0),
|
|
94
|
+
balance_difference: round2(line.balanceDifference || 0),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
78
97
|
|
|
79
98
|
// Client-side name search
|
|
80
99
|
if (args.employee_search) {
|
|
@@ -85,7 +104,12 @@ export async function handlePayrollLines(bcClient, args) {
|
|
|
85
104
|
// Summary totals
|
|
86
105
|
const summary = {
|
|
87
106
|
document_no,
|
|
107
|
+
payroll_type: header?.payrollType || null,
|
|
108
|
+
status: header?.status || null,
|
|
109
|
+
exchange_rate: rate || null,
|
|
88
110
|
total_employees: lines.length,
|
|
111
|
+
total_cost_usd: 0,
|
|
112
|
+
total_ves_equivalent: 0,
|
|
89
113
|
total_bonuses_ves: 0,
|
|
90
114
|
total_bonuses_usd: 0,
|
|
91
115
|
total_pay_ves: 0,
|
|
@@ -97,6 +121,8 @@ export async function handlePayrollLines(bcClient, args) {
|
|
|
97
121
|
};
|
|
98
122
|
|
|
99
123
|
for (const line of lines) {
|
|
124
|
+
summary.total_ves_equivalent += line.total_ves_equivalent;
|
|
125
|
+
if (line.cost_usd != null) summary.total_cost_usd += line.cost_usd;
|
|
100
126
|
summary.total_bonuses_ves += line.total_bonuses_ves;
|
|
101
127
|
summary.total_bonuses_usd += line.total_bonuses_usd;
|
|
102
128
|
summary.total_pay_ves += line.total_pay_ves;
|
|
@@ -107,6 +133,8 @@ export async function handlePayrollLines(bcClient, args) {
|
|
|
107
133
|
if (Math.abs(line.balance_difference) > 0.01) summary.employees_with_balance_diff++;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
summary.total_cost_usd = rate ? round2(summary.total_cost_usd) : null;
|
|
137
|
+
summary.total_ves_equivalent = round2(summary.total_ves_equivalent);
|
|
110
138
|
summary.total_bonuses_ves = round2(summary.total_bonuses_ves);
|
|
111
139
|
summary.total_bonuses_usd = round2(summary.total_bonuses_usd);
|
|
112
140
|
summary.total_pay_ves = round2(summary.total_pay_ves);
|
|
@@ -122,7 +150,3 @@ export async function handlePayrollLines(bcClient, args) {
|
|
|
122
150
|
lines,
|
|
123
151
|
};
|
|
124
152
|
}
|
|
125
|
-
|
|
126
|
-
function round2(n) {
|
|
127
|
-
return Math.round(n * 100) / 100;
|
|
128
|
-
}
|
|
@@ -99,7 +99,7 @@ export async function handleSalesAnalysis(bcClient, args) {
|
|
|
99
99
|
data.lines.map((l) => ({ ...l, storeCode: code, storeName: data.storeName }))
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
const storeSummary = calculateSummary(data.invoices, data.lines, exchangeRate, data.glRevenue);
|
|
102
|
+
const storeSummary = calculateSummary(data.invoices, data.lines, exchangeRate, data.glRevenue, code);
|
|
103
103
|
byStoreResults.push({
|
|
104
104
|
store_code: code,
|
|
105
105
|
store_name: data.storeName,
|
|
@@ -59,7 +59,7 @@ export async function handleSalesStoreComparison(bcClient, args) {
|
|
|
59
59
|
const storesSummaries = {};
|
|
60
60
|
|
|
61
61
|
for (const [code, data] of Object.entries(allStoresData)) {
|
|
62
|
-
const summary = calculateSummary(data.invoices, data.lines, exchangeRate, data.glRevenue);
|
|
62
|
+
const summary = calculateSummary(data.invoices, data.lines, exchangeRate, data.glRevenue, code);
|
|
63
63
|
storesSummaries[data.storeName] = { summary };
|
|
64
64
|
|
|
65
65
|
const storeResult = {
|
|
@@ -69,6 +69,10 @@ export async function handleSalesStoreComparison(bcClient, args) {
|
|
|
69
69
|
|
|
70
70
|
if (showAll || requestedMetrics.includes('revenue')) {
|
|
71
71
|
storeResult.revenue_usd = summary.total_revenue_usd;
|
|
72
|
+
storeResult.revenue_store = summary.revenue_store; // neto de interco, eventos y no-venta
|
|
73
|
+
storeResult.revenue_events = summary.revenue_events;
|
|
74
|
+
storeResult.revenue_intercompany = summary.revenue_intercompany;
|
|
75
|
+
storeResult.revenue_non_sales = summary.revenue_non_sales;
|
|
72
76
|
storeResult.revenue_breakdown = summary.revenue_breakdown;
|
|
73
77
|
storeResult.margin_usd = summary.total_margin_usd;
|
|
74
78
|
storeResult.margin_pct = summary.margin_pct;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getRevenueClassification } from '../config/income-accounts.js';
|
|
2
|
+
|
|
1
3
|
export function aggregateByField(lines, field) {
|
|
2
4
|
const map = {};
|
|
3
5
|
for (const line of lines) {
|
|
@@ -43,7 +45,7 @@ export function aggregateByDate(lines) {
|
|
|
43
45
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
export function calculateSummary(invoices, lines, exchangeRate = null, glRevenue = null) {
|
|
48
|
+
export function calculateSummary(invoices, lines, exchangeRate = null, glRevenue = null, storeCode = null) {
|
|
47
49
|
const totalTransactions = invoices.length;
|
|
48
50
|
const totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0);
|
|
49
51
|
const totalCostUSD = lines.reduce((sum, l) => sum + (l.unitCostUSD || 0) * (l.quantity || 0), 0);
|
|
@@ -55,8 +57,18 @@ export function calculateSummary(invoices, lines, exchangeRate = null, glRevenue
|
|
|
55
57
|
const marginPct = totalRevenueUSD && totalRevenueUSD !== 0
|
|
56
58
|
? (totalMarginUSD / totalRevenueUSD) * 100 : 0;
|
|
57
59
|
|
|
60
|
+
// Split revenue into the buckets that matter for the store-payroll KPI: store
|
|
61
|
+
// (own operation, the denominator) vs events (own ratio) vs intercompany (not a
|
|
62
|
+
// store sale) vs non-sales income (FX gains, purchase rebates). Store-aware.
|
|
63
|
+
const buckets = splitRevenueBuckets(glRevenue, totalRevenueUSD, storeCode);
|
|
64
|
+
|
|
58
65
|
return {
|
|
59
66
|
total_revenue_usd: totalRevenueUSD !== null ? Math.round(totalRevenueUSD * 100) / 100 : null,
|
|
67
|
+
revenue_total: buckets.revenue_total, // alias of total_revenue_usd (explicit)
|
|
68
|
+
revenue_store: buckets.revenue_store, // total − intercompany − events − non_sales
|
|
69
|
+
revenue_events: buckets.revenue_events,
|
|
70
|
+
revenue_intercompany: buckets.revenue_intercompany,
|
|
71
|
+
revenue_non_sales: buckets.revenue_non_sales, // FX gains + purchase rebates (excluded)
|
|
60
72
|
total_transactions: totalTransactions,
|
|
61
73
|
total_quantity: totalQuantity,
|
|
62
74
|
avg_ticket_usd: avgTicketUSD !== null ? Math.round(avgTicketUSD * 100) / 100 : null,
|
|
@@ -68,6 +80,28 @@ export function calculateSummary(invoices, lines, exchangeRate = null, glRevenue
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
// revenue_store = total − intercompany − events − non_sales. Accounts per store
|
|
84
|
+
// from config (not inline). Identity: total = store + interco + events + non_sales.
|
|
85
|
+
export function splitRevenueBuckets(glRevenue, totalRevenueUSD, storeCode = null) {
|
|
86
|
+
if (!glRevenue || !glRevenue.breakdown || totalRevenueUSD === null) {
|
|
87
|
+
return { revenue_total: totalRevenueUSD ?? null, revenue_store: null, revenue_events: null, revenue_intercompany: null, revenue_non_sales: null };
|
|
88
|
+
}
|
|
89
|
+
const b = glRevenue.breakdown;
|
|
90
|
+
const sumAccts = (accts) => accts.reduce((s, a) => s + (b[a] || 0), 0);
|
|
91
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
92
|
+
const { intercompany, events, nonSales } = getRevenueClassification(storeCode);
|
|
93
|
+
const ic = sumAccts(intercompany);
|
|
94
|
+
const ev = sumAccts(events);
|
|
95
|
+
const ns = sumAccts(nonSales);
|
|
96
|
+
return {
|
|
97
|
+
revenue_total: r2(totalRevenueUSD),
|
|
98
|
+
revenue_intercompany: r2(ic),
|
|
99
|
+
revenue_events: r2(ev),
|
|
100
|
+
revenue_non_sales: r2(ns),
|
|
101
|
+
revenue_store: r2(totalRevenueUSD - ic - ev - ns),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
71
105
|
export function calculateGrowth(current, previous) {
|
|
72
106
|
if (!previous || previous === 0) return null;
|
|
73
107
|
return ((current - previous) / Math.abs(previous)) * 100;
|