@fullqueso/mcp-bc-gastos 1.17.0 → 1.19.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/CHANGELOG.md +45 -1
- package/config/bank-gl-map.json +93 -0
- package/lib/bc-client.js +2 -2
- package/package.json +1 -1
- package/server.js +20 -0
- package/tools/auditoria/bank-ledger-entries.js +0 -2
- package/tools/auditoria/reconcile-pos-sales.js +39 -16
- package/tools/cobranzas/customer-list.js +63 -0
- package/tools/inventario/index.js +3 -0
- package/tools/inventario/inventory-by-location.js +212 -0
- package/tools/inventario/inventory-change.js +386 -0
- package/tools/inventario/inventory-levels.js +214 -0
- package/tools/inventario/item-card.js +1 -50
- package/tools/inventario/shared/cost-calculator.js +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [1.19.0] - 2026-04-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`get_customer_list` tool (cobranzas)** — Lists customers with number, name, and RIF (taxRegistrationNumber) per store. Supports optional filter by customer number.
|
|
7
|
+
- **`config/bank-gl-map.json`** — Reference config documenting the full bank account suffix → GL account mapping per store (FQ01, FQ28, FQ88), including GL structure for Caja y Bancos (181xx–189xx).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **POS reconciliation: per-store bank GL mapping** — `BANK_GL_MAP` in `reconcile-pos-sales.js` now keyed by store (FQ01, FQ28) instead of flat suffix lookup. Adds cash register accounts (MN0030, ME0030) and full FQ28 bank set. `getBankGLAccount()` now accepts store parameter for correct GL resolution.
|
|
11
|
+
- Tool count: 44 → 45
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [1.18.2] - 2026-04-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Inventory tools: exclude FQ-\* sale/combo categories** — `get_inventory_levels`, `get_inventory_change`, and `get_inventory_by_location` now filter out items with `itemCategoryCode` starting with `FQ-`, `FQ_`, or equal to `FQVENTA`. These are sales/combo products, not real inventory, and their BC `unitCost` can be contaminated (zero-inventory edge cases), producing inflated inventory valuations. Real inventory items (TERMINADO, INSUMO, etc.) keep BC's FIFO-based `unitCost` which is accurate.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Extracted `getCalculatedCosts` to shared module** — Moved from `item-card.js` to `tools/inventario/shared/cost-calculator.js` along with `isInventoryCategory()`. Avoids duplication across inventory tools.
|
|
22
|
+
- **Updated `calculated_unit_cost_method.md`** — Documented category filtering logic and clarified that calculated cost is for FQ-* items only; real inventory uses BC FIFO cost.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [1.18.1] - 2026-04-13
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `get_inventory_by_location` — Added `as_of_date` parameter for historical inventory reconstruction by location. Filters OData V4 item ledger entries with `Posting_Date <= as_of_date`. Also added `isInventoryCategory` filter to exclude FQ-* sale/combo items (aligns with `get_inventory_levels` behavior).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [1.18.0] - 2026-03-19
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
**Inventario domain — 2 new tools (inventory levels & change tracking):**
|
|
38
|
+
- `get_inventory_levels` — Inventory snapshot grouped by itemCategoryCode and inventoryPostingGroupCode (congelados/imported/local). Supports historical reconstruction via `as_of_date` (e.g., 1st of month post-EOM adjustment). Returns classification matrix with qty, cost value, and top items per group.
|
|
39
|
+
- `get_inventory_change` — WoW and MoM inventory change analysis. Reconstructs opening/closing inventory per period walking backwards from current balance. Movement breakdown by type (purchases, sales, adjustments, assembly). Trend summary per category and classification.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Tool count: 42 → 44
|
|
43
|
+
- Inventario domain: 4 → 6 tools
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
3
47
|
## [1.17.0] - 2026-03-18
|
|
4
48
|
|
|
5
49
|
### Highlights
|
|
6
50
|
- **Ventas domain consolidated** from `mcp-bc-analisis-ventas` — 3 new sales analysis tools
|
|
7
|
-
- Full Queso now has one unified BC MCP with
|
|
51
|
+
- Full Queso now has one unified BC MCP with **42 tools** across 8 domains
|
|
8
52
|
|
|
9
53
|
### Added
|
|
10
54
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"last_updated": "2026-03-23",
|
|
4
|
+
"description": "Mapeo banco_code (BC Bank Account suffix) → cuenta GL (Chart of Accounts). Usado por POS reconciliation y journal entry suggestions. Cada tienda tiene su propio CoA con bancos distintos.",
|
|
5
|
+
"stores": {
|
|
6
|
+
"FQ01": {
|
|
7
|
+
"store_name": "FQ01 - Chacao Sambil",
|
|
8
|
+
"accounts": {
|
|
9
|
+
"MN0001": { "gl": "18210", "name": "Banesco 4421 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
10
|
+
"MN0002": { "gl": "18220", "name": "BDV 1925 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
11
|
+
"MN0003": { "gl": "18230", "name": "Bancamiga 7015 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
12
|
+
"MN0004": { "gl": "18240", "name": "BDV 5550 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
13
|
+
"MN0005": { "gl": "18250", "name": "Bancamiga 4523 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
14
|
+
"MN0006": { "gl": "18260", "name": "Bancrecer 2450 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
15
|
+
"MN0007": { "gl": "18270", "name": "Bancrecer 2467 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
16
|
+
"MN0008": { "gl": "18280", "name": "BDV 5187 Bs. (Pago Móvil)", "currency": "VES", "category": "bancos_nacionales" },
|
|
17
|
+
"MN0012": { "gl": "18290", "name": "UBII", "currency": "VES", "category": "bancos_nacionales" },
|
|
18
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
19
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
20
|
+
"ME0006": { "gl": null, "name": "Zelle USD (Wells Fargo)", "currency": "USD", "category": "bancos_extranjeros", "_nota": "GL pendiente de confirmar" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"FQ28": {
|
|
24
|
+
"store_name": "FQ28 - Marqués El Unicentro",
|
|
25
|
+
"accounts": {
|
|
26
|
+
"MN0001": { "gl": "18210", "name": "BDV 8139 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
27
|
+
"MN0002": { "gl": "18220", "name": "Bancrecer 5474 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
28
|
+
"MN0028": { "gl": "18290", "name": "UBII 6249 Bs", "currency": "VES", "category": "bancos_nacionales" },
|
|
29
|
+
"MN0029": { "gl": "18291", "name": "UBII 6442 * Bs", "currency": "VES", "category": "bancos_nacionales" },
|
|
30
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
31
|
+
"MN0031": { "gl": "18160", "name": "Caja transitoria tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
32
|
+
"MN0032": { "gl": "18292", "name": "UBII 4 FQ-28", "currency": "VES", "category": "bancos_nacionales" },
|
|
33
|
+
"MN0033": { "gl": "18295", "name": "UBII 7 FQ-28", "currency": "VES", "category": "bancos_nacionales" },
|
|
34
|
+
"MN0098": { "gl": "18298", "name": "Operaciones en Tránsito", "currency": "VES", "category": "bancos_nacionales" },
|
|
35
|
+
"ME0002": { "gl": "18310", "name": "Bancrecer $", "currency": "USD", "category": "bancos_nacionales_divisas" },
|
|
36
|
+
"ME0005": { "gl": "18410", "name": "Zelle Bofa $ *", "currency": "USD", "category": "bancos_extranjeros" },
|
|
37
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
38
|
+
"ME0031": { "gl": "18170", "name": "Caja transitoria tienda ventas $", "currency": "USD", "category": "caja" },
|
|
39
|
+
"ME0035": { "gl": "18150", "name": "Banco Bóveda $", "currency": "USD", "category": "caja" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"FQ88": {
|
|
43
|
+
"store_name": "FQ88 - La Candelaria",
|
|
44
|
+
"_nota": "GL accounts pendientes de confirmar con CoA de FQ88",
|
|
45
|
+
"accounts": {
|
|
46
|
+
"MN0001": { "gl": null, "name": "Bancamiga 0240 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
47
|
+
"MN0002": { "gl": null, "name": "Bancamiga 9379 Bs. *", "currency": "VES", "category": "bancos_nacionales" },
|
|
48
|
+
"MN0003": { "gl": null, "name": "BDV 9127 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
49
|
+
"MN0004": { "gl": null, "name": "BDV 7191 Bs. *", "currency": "VES", "category": "bancos_nacionales" },
|
|
50
|
+
"MN0005": { "gl": null, "name": "Bancrecer 2558 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
51
|
+
"MN0007": { "gl": null, "name": "BDV Pago Movil 5145", "currency": "VES", "category": "bancos_nacionales" },
|
|
52
|
+
"MN0028": { "gl": null, "name": "Ubii Bank", "currency": "VES", "category": "bancos_nacionales" },
|
|
53
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
54
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
55
|
+
"ME0005": { "gl": null, "name": "Zelle USD", "currency": "USD", "category": "bancos_extranjeros" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"gl_structure": {
|
|
60
|
+
"18100": "Caja (header)",
|
|
61
|
+
"18110": "Caja chica VEB",
|
|
62
|
+
"18120": "Caja chica $",
|
|
63
|
+
"18130": "Caja tienda ventas VEB",
|
|
64
|
+
"18140": "Caja tienda ventas $",
|
|
65
|
+
"18150": "Bóveda $",
|
|
66
|
+
"18160": "Caja tránsito tienda ventas VEB",
|
|
67
|
+
"18170": "Caja tránsito tienda ventas $",
|
|
68
|
+
"18199": "Total Caja",
|
|
69
|
+
"18200": "Bancos Nacionales Bolívares (header)",
|
|
70
|
+
"18210": "Banco principal Bs.",
|
|
71
|
+
"18220": "Banco secundario Bs.",
|
|
72
|
+
"18230": "Banco terciario Bs.",
|
|
73
|
+
"18240": "Banco cuarto Bs.",
|
|
74
|
+
"18250": "Banco quinto Bs.",
|
|
75
|
+
"18260": "Banco sexto Bs.",
|
|
76
|
+
"18270": "Banco séptimo Bs.",
|
|
77
|
+
"18280": "Banco octavo Bs.",
|
|
78
|
+
"18290": "UBII principal",
|
|
79
|
+
"18291": "UBII secundario",
|
|
80
|
+
"18292": "UBII terciario",
|
|
81
|
+
"18295": "UBII cuarto",
|
|
82
|
+
"18298": "Operaciones en Tránsito",
|
|
83
|
+
"18299": "Total Bancos Nacionales",
|
|
84
|
+
"18300": "Bancos Nacionales Divisas (header)",
|
|
85
|
+
"18310": "Banco divisas principal",
|
|
86
|
+
"18399": "Total Bancos Nacionales Divisas",
|
|
87
|
+
"18400": "Bancos Extranjeros Divisas (header)",
|
|
88
|
+
"18410": "Banco extranjero principal",
|
|
89
|
+
"18499": "Total Bancos Extranjeros Divisas",
|
|
90
|
+
"18998": "Total Bancos",
|
|
91
|
+
"18999": "Total Caja y Bancos"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/lib/bc-client.js
CHANGED
|
@@ -567,7 +567,7 @@ export class BCClient {
|
|
|
567
567
|
|
|
568
568
|
async getItemsCatalog(companyId) {
|
|
569
569
|
const url = this.buildApiUrl(companyId, 'items', {
|
|
570
|
-
$select: 'id,number,displayName,type,unitPrice,unitCost,itemCategoryCode
|
|
570
|
+
$select: 'id,number,displayName,type,unitPrice,unitCost,itemCategoryCode',
|
|
571
571
|
});
|
|
572
572
|
return this.apiCallAllPages(url);
|
|
573
573
|
}
|
|
@@ -619,7 +619,7 @@ export class BCClient {
|
|
|
619
619
|
const item = itemsMap[itemNo];
|
|
620
620
|
if (item) {
|
|
621
621
|
line.itemCategoryCode = item.itemCategoryCode;
|
|
622
|
-
line.unitCostUSD = item.
|
|
622
|
+
line.unitCostUSD = item.unitCost || 0;
|
|
623
623
|
line.displayName = item.displayName || line.description;
|
|
624
624
|
}
|
|
625
625
|
if (!line.displayName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullqueso/mcp-bc-gastos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable, multi-payment draft visibility, payroll, inventory cost analysis, and manager reports - Full Queso franchise stores",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -54,6 +54,7 @@ import { openReceivablesTool, handleOpenReceivables } from './tools/cobranzas/op
|
|
|
54
54
|
import { collectionStatusTool, handleCollectionStatus } from './tools/cobranzas/collection-status.js';
|
|
55
55
|
import { vendorLedgerTool, handleVendorLedger } from './tools/cobranzas/vendor-ledger.js';
|
|
56
56
|
import { openPayablesTool, handleOpenPayables } from './tools/cobranzas/open-payables.js';
|
|
57
|
+
import { customerListTool, handleCustomerList } from './tools/cobranzas/customer-list.js';
|
|
57
58
|
|
|
58
59
|
// Multi-Payment draft visibility tools
|
|
59
60
|
import {
|
|
@@ -75,6 +76,9 @@ import {
|
|
|
75
76
|
itemLedgerEntriesTool, handleItemLedgerEntries,
|
|
76
77
|
itemValueEntriesTool, handleItemValueEntries,
|
|
77
78
|
itemCostTrendTool, handleItemCostTrend,
|
|
79
|
+
inventoryLevelsTool, handleInventoryLevels,
|
|
80
|
+
inventoryChangeTool, handleInventoryChange,
|
|
81
|
+
inventoryByLocationTool, handleInventoryByLocation,
|
|
78
82
|
} from './tools/inventario/index.js';
|
|
79
83
|
|
|
80
84
|
// Ventas tools (sales analysis)
|
|
@@ -136,6 +140,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
136
140
|
collectionStatusTool,
|
|
137
141
|
vendorLedgerTool,
|
|
138
142
|
openPayablesTool,
|
|
143
|
+
customerListTool,
|
|
139
144
|
// Multi-Payment draft visibility
|
|
140
145
|
draftReceivablesTool,
|
|
141
146
|
draftPayablesTool,
|
|
@@ -149,6 +154,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
149
154
|
itemLedgerEntriesTool,
|
|
150
155
|
itemValueEntriesTool, // get_item_cost_analysis
|
|
151
156
|
itemCostTrendTool,
|
|
157
|
+
inventoryLevelsTool,
|
|
158
|
+
inventoryChangeTool,
|
|
159
|
+
inventoryByLocationTool,
|
|
152
160
|
// Ventas (sales analysis)
|
|
153
161
|
salesAnalysisTool,
|
|
154
162
|
productPerformanceTool,
|
|
@@ -251,6 +259,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
251
259
|
case 'get_open_payables':
|
|
252
260
|
result = await handleOpenPayables(bcClient, args);
|
|
253
261
|
break;
|
|
262
|
+
case 'get_customer_list':
|
|
263
|
+
result = await handleCustomerList(bcClient, args);
|
|
264
|
+
break;
|
|
254
265
|
// Multi-Payment draft visibility
|
|
255
266
|
case 'get_draft_receivables':
|
|
256
267
|
result = await handleDraftReceivables(bcClient, args);
|
|
@@ -284,6 +295,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
284
295
|
case 'get_item_cost_trend':
|
|
285
296
|
result = await handleItemCostTrend(bcClient, args);
|
|
286
297
|
break;
|
|
298
|
+
case 'get_inventory_levels':
|
|
299
|
+
result = await handleInventoryLevels(bcClient, args);
|
|
300
|
+
break;
|
|
301
|
+
case 'get_inventory_change':
|
|
302
|
+
result = await handleInventoryChange(bcClient, args);
|
|
303
|
+
break;
|
|
304
|
+
case 'get_inventory_by_location':
|
|
305
|
+
result = await handleInventoryByLocation(bcClient, args);
|
|
306
|
+
break;
|
|
287
307
|
// Ventas (sales analysis)
|
|
288
308
|
case 'get_sales_analysis':
|
|
289
309
|
result = await handleSalesAnalysis(bcClient, args);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DateTime } from 'luxon';
|
|
2
1
|
import { resolveStores } from '../../config/company-config.js';
|
|
3
2
|
|
|
4
3
|
export const bankLedgerEntriesTool = {
|
|
@@ -60,7 +59,6 @@ export async function handleBankLedgerEntries(bcClient, args) {
|
|
|
60
59
|
});
|
|
61
60
|
const entries = await bcClient.odataCallAllPages(url);
|
|
62
61
|
|
|
63
|
-
const today = DateTime.now();
|
|
64
62
|
const formatted = entries.map((e) => {
|
|
65
63
|
const amount = e.Amount || 0;
|
|
66
64
|
const postingDate = e.Posting_Date || null;
|
|
@@ -4,17 +4,37 @@ import { logger } from '../../utils/logger.js';
|
|
|
4
4
|
|
|
5
5
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
-
// Bank account suffix → GL account for journal entry suggestions
|
|
7
|
+
// Bank account suffix → GL account for journal entry suggestions (per store)
|
|
8
8
|
const BANK_GL_MAP = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
FQ01: {
|
|
10
|
+
'MN0001': '18210', // Banesco 4421 Bs.
|
|
11
|
+
'MN0002': '18220', // BDV 1925 Bs.
|
|
12
|
+
'MN0003': '18230', // Bancamiga 7015 Bs.
|
|
13
|
+
'MN0004': '18240', // BDV 5550 Bs.
|
|
14
|
+
'MN0005': '18250', // Bancamiga 4523 Bs.
|
|
15
|
+
'MN0006': '18260', // Bancrecer 2450 Bs.
|
|
16
|
+
'MN0007': '18270', // Bancrecer 2467 Bs.
|
|
17
|
+
'MN0008': '18280', // BDV 5187 Bs. (Pago Móvil)
|
|
18
|
+
'MN0012': '18290', // UBII
|
|
19
|
+
'MN0030': '18130', // Caja tienda ventas Bs.
|
|
20
|
+
'ME0030': '18140', // Caja tienda ventas $
|
|
21
|
+
},
|
|
22
|
+
FQ28: {
|
|
23
|
+
'MN0001': '18210', // BDV 8139 Bs.
|
|
24
|
+
'MN0002': '18220', // Bancrecer 5474 Bs.
|
|
25
|
+
'MN0028': '18290', // UBII 6249 Bs
|
|
26
|
+
'MN0029': '18291', // UBII 6442 * Bs
|
|
27
|
+
'MN0030': '18130', // Caja tienda ventas Bs.
|
|
28
|
+
'MN0031': '18160', // Caja transitoria tienda ventas Bs.
|
|
29
|
+
'MN0032': '18292', // UBII 4 FQ-28
|
|
30
|
+
'MN0033': '18295', // UBII 7 FQ-28
|
|
31
|
+
'MN0098': '18298', // Operaciones en Tránsito
|
|
32
|
+
'ME0002': '18310', // Bancrecer $
|
|
33
|
+
'ME0005': '18410', // Zelle Bofa $ *
|
|
34
|
+
'ME0030': '18140', // Caja tienda ventas $
|
|
35
|
+
'ME0031': '18170', // Caja transitoria tienda ventas $
|
|
36
|
+
'ME0035': '18150', // Banco Bóveda $
|
|
37
|
+
},
|
|
18
38
|
};
|
|
19
39
|
|
|
20
40
|
const COMMISSION_ACCOUNT = '67100'; // Comisiones Bancarias
|
|
@@ -140,12 +160,15 @@ function parseBankStatementLot(description) {
|
|
|
140
160
|
* Get GL account for a bank account number.
|
|
141
161
|
* "FQ01-MN0001" → "18210"
|
|
142
162
|
*/
|
|
143
|
-
function getBankGLAccount(bankAccountNo) {
|
|
163
|
+
function getBankGLAccount(bankAccountNo, store) {
|
|
144
164
|
if (!bankAccountNo) return null;
|
|
145
|
-
// Extract suffix: "FQ01-MN0001" → "MN0001"
|
|
165
|
+
// Extract store and suffix: "FQ01-MN0001" → store "FQ01", suffix "MN0001"
|
|
146
166
|
const parts = bankAccountNo.split('-');
|
|
147
167
|
const suffix = parts[parts.length - 1];
|
|
148
|
-
|
|
168
|
+
const storeKey = store || parts[0];
|
|
169
|
+
const storeMap = BANK_GL_MAP[storeKey];
|
|
170
|
+
if (!storeMap) return null;
|
|
171
|
+
return storeMap[suffix] || null;
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
// ── Union-Find for Compound Lots ─────────────────────────────────────────
|
|
@@ -818,7 +841,7 @@ export async function handleReconcilePOSSales(bcClient, args) {
|
|
|
818
841
|
}
|
|
819
842
|
|
|
820
843
|
// ── Journal entry suggestions for commissions ──
|
|
821
|
-
const glAccount = getBankGLAccount(bankAccount);
|
|
844
|
+
const glAccount = getBankGLAccount(bankAccount, store);
|
|
822
845
|
const journalEntries = [];
|
|
823
846
|
|
|
824
847
|
if (isBDVAggregate && bdvAggregate.commission_total > 0) {
|
|
@@ -1211,7 +1234,7 @@ export async function handleReconcilePOSSales(bcClient, args) {
|
|
|
1211
1234
|
const ubiiAvgFee = ubiiTotalBC > 0 ? round2((ubiiTotalFee / ubiiTotalBC) * 100) : 0;
|
|
1212
1235
|
|
|
1213
1236
|
// Journal entries: fee + transfer per matched day
|
|
1214
|
-
const ubiiGL = getBankGLAccount(ubiiFullAccount); // 18290
|
|
1237
|
+
const ubiiGL = getBankGLAccount(ubiiFullAccount, store); // 18290
|
|
1215
1238
|
const ubiiJournalEntries = [];
|
|
1216
1239
|
for (const m of ubiiMatchedDays) {
|
|
1217
1240
|
if (m.fee_ves > 0) {
|
|
@@ -1225,7 +1248,7 @@ export async function handleReconcilePOSSales(bcClient, args) {
|
|
|
1225
1248
|
});
|
|
1226
1249
|
}
|
|
1227
1250
|
// Transfer entry: debit receiving bank, credit UBII
|
|
1228
|
-
const receivingGL = getBankGLAccount(m.bank_deposit_bank);
|
|
1251
|
+
const receivingGL = getBankGLAccount(m.bank_deposit_bank, store);
|
|
1229
1252
|
if (receivingGL) {
|
|
1230
1253
|
ubiiJournalEntries.push({
|
|
1231
1254
|
posting_date: m.bank_deposit_date,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolveStores } from '../../config/company-config.js';
|
|
2
|
+
|
|
3
|
+
export const customerListTool = {
|
|
4
|
+
name: 'get_customer_list',
|
|
5
|
+
description:
|
|
6
|
+
'Lista de clientes con número, nombre y RIF (taxRegistrationNumber). Útil para identificar clientes por RIF o exportar directorio de clientes.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
store: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
enum: ['FQ01', 'FQ28', 'FQ88'],
|
|
13
|
+
description: 'Tienda a consultar.',
|
|
14
|
+
},
|
|
15
|
+
customer_number: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Filtrar por número de cliente específico.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['store'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function handleCustomerList(bcClient, args) {
|
|
25
|
+
const store = args.store;
|
|
26
|
+
if (!store) throw new Error('Parámetro requerido: store');
|
|
27
|
+
|
|
28
|
+
const stores = resolveStores([store]);
|
|
29
|
+
const storeInfo = stores[0];
|
|
30
|
+
|
|
31
|
+
const selectFields = 'number,displayName,taxRegistrationNumber';
|
|
32
|
+
const params = { $select: selectFields, $orderby: 'number' };
|
|
33
|
+
|
|
34
|
+
if (args.customer_number) {
|
|
35
|
+
params.$filter = `number eq '${args.customer_number}'`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const url = bcClient.buildApiUrl(storeInfo.companyId, 'customers', params);
|
|
39
|
+
const allCustomers = await bcClient.apiCallAllPages(url);
|
|
40
|
+
|
|
41
|
+
const customers = allCustomers.map((c) => ({
|
|
42
|
+
number: c.number,
|
|
43
|
+
name: c.displayName,
|
|
44
|
+
rif: c.taxRegistrationNumber || '',
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: JSON.stringify(
|
|
52
|
+
{
|
|
53
|
+
store,
|
|
54
|
+
total_customers: customers.length,
|
|
55
|
+
customers,
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -2,3 +2,6 @@ export { itemCardTool, handleItemCard } from './item-card.js';
|
|
|
2
2
|
export { itemLedgerEntriesTool, handleItemLedgerEntries } from './item-ledger-entries.js';
|
|
3
3
|
export { itemValueEntriesTool, handleItemValueEntries } from './item-value-entries.js';
|
|
4
4
|
export { itemCostTrendTool, handleItemCostTrend } from './item-cost-trend.js';
|
|
5
|
+
export { inventoryLevelsTool, handleInventoryLevels } from './inventory-levels.js';
|
|
6
|
+
export { inventoryChangeTool, handleInventoryChange } from './inventory-change.js';
|
|
7
|
+
export { inventoryByLocationTool, handleInventoryByLocation } from './inventory-by-location.js';
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { resolveStores } from '../../config/company-config.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { isInventoryCategory } from './shared/cost-calculator.js';
|
|
4
|
+
|
|
5
|
+
export const inventoryByLocationTool = {
|
|
6
|
+
name: 'get_inventory_by_location',
|
|
7
|
+
description:
|
|
8
|
+
'Inventario desglosado por ubicación (Location Code) dentro de una tienda. Muestra qty y valor por location, con detalle de ítems por cada una. Útil para ver distribución entre depósitos/almacenes internos. Usa OData V4 para acceder a Location_Code.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
store: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['FQ01', 'FQ28', 'FQ88'],
|
|
15
|
+
description: 'Tienda a consultar.',
|
|
16
|
+
},
|
|
17
|
+
location_code: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Filtrar por un Location Code específico (e.g. PRINCIPAL, DEPOSITO). Si se omite, muestra todas las ubicaciones.',
|
|
20
|
+
},
|
|
21
|
+
item_category: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Filtrar por itemCategoryCode (e.g. TERMINADO, INSUMO).',
|
|
24
|
+
},
|
|
25
|
+
classification: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Filtrar por inventoryPostingGroupCode (e.g. CONGELADOS, IMPORTADO, LOCAL).',
|
|
28
|
+
},
|
|
29
|
+
as_of_date: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description:
|
|
32
|
+
'Fecha de corte YYYY-MM-DD. Default: hoy (todas las entries). Si es fecha pasada, solo suma ledger entries con Posting_Date <= as_of_date.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['store'],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function handleInventoryByLocation(bcClient, args) {
|
|
40
|
+
const storeParam = args.store;
|
|
41
|
+
if (!storeParam) throw new Error('Parámetro requerido: store');
|
|
42
|
+
|
|
43
|
+
const stores = resolveStores([storeParam]);
|
|
44
|
+
const store = stores[0];
|
|
45
|
+
const companyId = store.companyId;
|
|
46
|
+
|
|
47
|
+
// 1. Fetch items via standard API (has itemCategoryCode, inventoryPostingGroupCode, unitCost)
|
|
48
|
+
const itemFilters = [];
|
|
49
|
+
if (args.item_category) {
|
|
50
|
+
itemFilters.push(`itemCategoryCode eq '${args.item_category}'`);
|
|
51
|
+
}
|
|
52
|
+
if (args.classification) {
|
|
53
|
+
itemFilters.push(`inventoryPostingGroupCode eq '${args.classification}'`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const itemParams = {
|
|
57
|
+
$select: 'number,displayName,itemCategoryCode,inventoryPostingGroupCode,unitCost,inventory',
|
|
58
|
+
};
|
|
59
|
+
if (itemFilters.length > 0) {
|
|
60
|
+
itemParams.$filter = itemFilters.join(' and ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const itemUrl = bcClient.buildApiUrl(companyId, 'items', itemParams);
|
|
64
|
+
const items = await bcClient.apiCallAllPages(itemUrl);
|
|
65
|
+
logger.info(`${store.code}: ${items.length} items fetched`);
|
|
66
|
+
|
|
67
|
+
// Build item metadata map — exclude items without inventoryPostingGroupCode and FQ-* categories
|
|
68
|
+
const itemMeta = {};
|
|
69
|
+
let excludedCount = 0;
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (!item.inventoryPostingGroupCode || !isInventoryCategory(item.itemCategoryCode)) {
|
|
72
|
+
excludedCount++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
itemMeta[item.number] = {
|
|
76
|
+
name: item.displayName,
|
|
77
|
+
category: item.itemCategoryCode || 'SIN_CATEGORIA',
|
|
78
|
+
classification: item.inventoryPostingGroupCode,
|
|
79
|
+
unit_cost: item.unitCost || 0,
|
|
80
|
+
total_inventory: item.inventory || 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (excludedCount > 0) {
|
|
84
|
+
logger.info(`${store.code}: excluded ${excludedCount} items without inventoryPostingGroupCode`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Fetch item ledger entries via OData V4 (has Location_Code field)
|
|
88
|
+
const today = new Date();
|
|
89
|
+
const todayStr = today.toISOString().split('T')[0];
|
|
90
|
+
const asOfDate = args.as_of_date || todayStr;
|
|
91
|
+
const isHistorical = asOfDate < todayStr;
|
|
92
|
+
|
|
93
|
+
const odataFilters = [];
|
|
94
|
+
if (args.location_code) {
|
|
95
|
+
odataFilters.push(`Location_Code eq '${args.location_code}'`);
|
|
96
|
+
}
|
|
97
|
+
if (isHistorical) {
|
|
98
|
+
odataFilters.push(`Posting_Date le ${asOfDate}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const odataParams = {
|
|
102
|
+
$select: 'Item_No,Location_Code,Quantity',
|
|
103
|
+
};
|
|
104
|
+
if (odataFilters.length > 0) {
|
|
105
|
+
odataParams.$filter = odataFilters.join(' and ');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ledgerUrl = bcClient.buildODataUrl(storeParam, 'ItemLedgerEntries', odataParams);
|
|
109
|
+
const entries = await bcClient.odataCallAllPages(ledgerUrl);
|
|
110
|
+
logger.info(`${store.code}: ${entries.length} item ledger entries fetched via OData V4 for location breakdown`);
|
|
111
|
+
|
|
112
|
+
// 3. Aggregate: { Location_Code → { Item_No → net_qty } }
|
|
113
|
+
const locationItems = {};
|
|
114
|
+
|
|
115
|
+
for (const e of entries) {
|
|
116
|
+
const loc = e.Location_Code || 'SIN_UBICACION';
|
|
117
|
+
const itemNo = e.Item_No;
|
|
118
|
+
const qty = e.Quantity || 0;
|
|
119
|
+
|
|
120
|
+
// Skip items not in our metadata (filtered out or no posting group)
|
|
121
|
+
if (!itemMeta[itemNo]) continue;
|
|
122
|
+
|
|
123
|
+
if (!locationItems[loc]) locationItems[loc] = {};
|
|
124
|
+
if (!locationItems[loc][itemNo]) locationItems[loc][itemNo] = 0;
|
|
125
|
+
locationItems[loc][itemNo] += qty;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 4. Build response per location
|
|
129
|
+
const byLocation = {};
|
|
130
|
+
const locationSummaries = [];
|
|
131
|
+
|
|
132
|
+
for (const [loc, itemQtys] of Object.entries(locationItems)) {
|
|
133
|
+
let itemCount = 0;
|
|
134
|
+
let totalQty = 0;
|
|
135
|
+
let totalCostValue = 0;
|
|
136
|
+
const itemDetails = [];
|
|
137
|
+
|
|
138
|
+
for (const [itemNo, netQty] of Object.entries(itemQtys)) {
|
|
139
|
+
const roundedQty = round5(netQty);
|
|
140
|
+
if (roundedQty === 0) continue;
|
|
141
|
+
|
|
142
|
+
const meta = itemMeta[itemNo];
|
|
143
|
+
const costValue = round2(roundedQty * meta.unit_cost);
|
|
144
|
+
|
|
145
|
+
itemCount++;
|
|
146
|
+
totalQty = round5(totalQty + roundedQty);
|
|
147
|
+
totalCostValue = round2(totalCostValue + costValue);
|
|
148
|
+
|
|
149
|
+
itemDetails.push({
|
|
150
|
+
number: itemNo,
|
|
151
|
+
name: meta.name,
|
|
152
|
+
category: meta.category,
|
|
153
|
+
classification: meta.classification,
|
|
154
|
+
qty: roundedQty,
|
|
155
|
+
unit_cost: meta.unit_cost,
|
|
156
|
+
cost_value: costValue,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (itemCount === 0) continue;
|
|
161
|
+
|
|
162
|
+
// Sort by absolute qty descending, keep top 10
|
|
163
|
+
itemDetails.sort((a, b) => Math.abs(b.qty) - Math.abs(a.qty));
|
|
164
|
+
const topItems = itemDetails.slice(0, 10);
|
|
165
|
+
|
|
166
|
+
byLocation[loc] = {
|
|
167
|
+
item_count: itemCount,
|
|
168
|
+
total_qty: round5(totalQty),
|
|
169
|
+
total_cost_value: round2(totalCostValue),
|
|
170
|
+
top_items: topItems,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
locationSummaries.push({
|
|
174
|
+
location: loc,
|
|
175
|
+
item_count: itemCount,
|
|
176
|
+
total_qty: round5(totalQty),
|
|
177
|
+
total_cost_value: round2(totalCostValue),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sort summaries by cost value descending
|
|
182
|
+
locationSummaries.sort((a, b) => b.total_cost_value - a.total_cost_value);
|
|
183
|
+
|
|
184
|
+
// Grand totals
|
|
185
|
+
const grandTotal = {
|
|
186
|
+
locations: locationSummaries.length,
|
|
187
|
+
item_count: locationSummaries.reduce((s, l) => s + l.item_count, 0),
|
|
188
|
+
total_qty: round5(locationSummaries.reduce((s, l) => s + l.total_qty, 0)),
|
|
189
|
+
total_cost_value: round2(locationSummaries.reduce((s, l) => s + l.total_cost_value, 0)),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
store: store.code,
|
|
194
|
+
store_name: store.name,
|
|
195
|
+
as_of_date: asOfDate,
|
|
196
|
+
is_historical: isHistorical,
|
|
197
|
+
note: isHistorical
|
|
198
|
+
? `Inventario histórico al ${asOfDate} reconstruido desde item ledger entries (OData V4) con Posting_Date <= ${asOfDate}, agrupado por Location_Code. unitCost es el actual de BC (no histórico).`
|
|
199
|
+
: 'Inventario reconstruido desde item ledger entries (OData V4) agrupado por Location_Code. Cada location muestra top 10 ítems por qty. unitCost es el actual de BC.',
|
|
200
|
+
summary: locationSummaries,
|
|
201
|
+
by_location: byLocation,
|
|
202
|
+
grand_total: grandTotal,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function round2(n) {
|
|
207
|
+
return Math.round(n * 100) / 100;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function round5(n) {
|
|
211
|
+
return Math.round(n * 100000) / 100000;
|
|
212
|
+
}
|