From c341726ddaf62ed7ee54a2269ec8111c2273e0f3 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 17 Feb 2026 06:31:54 +0000 Subject: [PATCH] Add catalog import service for managing part number cross-references Implement CSV/Excel catalog import using xlsx, with search and manual update capabilities. Add UNIQUE constraint on catalogo_intercambios .no_parte_proveedor to enable INSERT OR REPLACE semantics correctly. Importing a catalog also updates intercambio on existing conceptos. Co-Authored-By: Claude Opus 4.6 --- src/services/catalog-service.js | 54 ++++++++++++++++++++++++ src/services/database.js | 4 +- tests/catalog-service.test.js | 68 +++++++++++++++++++++++++++++++ tests/fixtures/sample-catalog.csv | 4 ++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/services/catalog-service.js create mode 100644 tests/catalog-service.test.js create mode 100644 tests/fixtures/sample-catalog.csv diff --git a/src/services/catalog-service.js b/src/services/catalog-service.js new file mode 100644 index 0000000..c4f4efb --- /dev/null +++ b/src/services/catalog-service.js @@ -0,0 +1,54 @@ +const fs = require('fs'); +const XLSX = require('xlsx'); + +function importCatalog(db, filePath) { + const workbook = XLSX.readFile(filePath); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + const rows = XLSX.utils.sheet_to_json(sheet); + + const insert = db.prepare( + 'INSERT OR REPLACE INTO catalogo_intercambios (no_parte_proveedor, no_parte_intercambio) VALUES (?, ?)' + ); + + const updateConceptos = db.prepare( + 'UPDATE conceptos SET intercambio = ? WHERE no_identificacion = ?' + ); + + const transaction = db.transaction(() => { + let imported = 0; + for (const row of rows) { + const proveedor = row.no_parte_proveedor || row['No. Parte Proveedor'] || ''; + const intercambio = row.no_parte_intercambio || row['No. Parte Intercambio'] || ''; + + if (proveedor && intercambio) { + insert.run(proveedor.toString().trim(), intercambio.toString().trim()); + updateConceptos.run(intercambio.toString().trim(), proveedor.toString().trim()); + imported++; + } + } + return imported; + }); + + const imported = transaction(); + return { imported }; +} + +function searchCatalog(db, query) { + return db.prepare( + `SELECT * FROM catalogo_intercambios + WHERE no_parte_proveedor LIKE ? OR no_parte_intercambio LIKE ? + LIMIT 100` + ).all(`%${query}%`, `%${query}%`); +} + +function updateIntercambio(db, noParteProveedor, noParteIntercambio) { + db.prepare( + 'UPDATE catalogo_intercambios SET no_parte_intercambio = ? WHERE no_parte_proveedor = ?' + ).run(noParteIntercambio, noParteProveedor); + + db.prepare( + 'UPDATE conceptos SET intercambio = ? WHERE no_identificacion = ?' + ).run(noParteIntercambio, noParteProveedor); +} + +module.exports = { importCatalog, searchCatalog, updateIntercambio }; diff --git a/src/services/database.js b/src/services/database.js index 190087f..a91cfe6 100644 --- a/src/services/database.js +++ b/src/services/database.js @@ -34,7 +34,7 @@ function createDatabase(dbPath) { CREATE TABLE IF NOT EXISTS catalogo_intercambios ( id INTEGER PRIMARY KEY AUTOINCREMENT, - no_parte_proveedor TEXT NOT NULL, + no_parte_proveedor TEXT NOT NULL UNIQUE, no_parte_intercambio TEXT NOT NULL ); @@ -45,7 +45,7 @@ function createDatabase(dbPath) { CREATE INDEX IF NOT EXISTS idx_conceptos_factura ON conceptos(factura_id); CREATE INDEX IF NOT EXISTS idx_conceptos_no_id ON conceptos(no_identificacion); - CREATE INDEX IF NOT EXISTS idx_catalogo_parte ON catalogo_intercambios(no_parte_proveedor); + CREATE UNIQUE INDEX IF NOT EXISTS idx_catalogo_parte ON catalogo_intercambios(no_parte_proveedor); `); const insertConfig = db.prepare( diff --git a/tests/catalog-service.test.js b/tests/catalog-service.test.js new file mode 100644 index 0000000..0efa4d4 --- /dev/null +++ b/tests/catalog-service.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +describe('Catalog Service', () => { + let db; + + beforeEach(async () => { + const { createDatabase } = await import('../src/services/database.js'); + db = createDatabase(':memory:'); + }); + + it('should import CSV catalog', async () => { + const { importCatalog } = await import('../src/services/catalog-service.js'); + const csvPath = path.join(__dirname, 'fixtures', 'sample-catalog.csv'); + const result = importCatalog(db, csvPath); + + expect(result.imported).toBe(3); + + const all = db.prepare('SELECT * FROM catalogo_intercambios').all(); + expect(all).toHaveLength(3); + }); + + it('should search catalog by part number', async () => { + const { searchCatalog } = await import('../src/services/catalog-service.js'); + db.prepare( + 'INSERT INTO catalogo_intercambios (no_parte_proveedor, no_parte_intercambio) VALUES (?, ?)' + ).run('BK-001', 'INT-9999'); + + const results = searchCatalog(db, 'BK'); + expect(results).toHaveLength(1); + expect(results[0].no_parte_proveedor).toBe('BK-001'); + }); + + it('should update intercambio for existing conceptos when catalog is imported', async () => { + const { importCatalog } = await import('../src/services/catalog-service.js'); + + db.prepare(` + INSERT INTO facturas (uuid, rfc_emisor, nombre_emisor, fecha, subtotal, total, xml_path) + VALUES ('UUID-1', 'RFC1', 'Prov', '2026-01-01', 100, 116, '/x.xml') + `).run(); + const facturaId = db.prepare('SELECT id FROM facturas WHERE uuid = ?').get('UUID-1').id; + db.prepare(` + INSERT INTO conceptos (factura_id, no_identificacion, descripcion, cantidad, valor_unitario, importe) + VALUES (?, 'BK-001', 'Balata', 1, 100, 100) + `).run(facturaId); + + const csvPath = path.join(__dirname, 'fixtures', 'sample-catalog.csv'); + importCatalog(db, csvPath); + + const concepto = db.prepare("SELECT * FROM conceptos WHERE no_identificacion = 'BK-001'").get(); + expect(concepto.intercambio).toBe('INT-9999'); + }); + + it('should manually update a single intercambio', async () => { + const { updateIntercambio } = await import('../src/services/catalog-service.js'); + db.prepare( + 'INSERT INTO catalogo_intercambios (no_parte_proveedor, no_parte_intercambio) VALUES (?, ?)' + ).run('BK-001', 'OLD'); + + updateIntercambio(db, 'BK-001', 'NEW-INT'); + + const row = db.prepare("SELECT * FROM catalogo_intercambios WHERE no_parte_proveedor = 'BK-001'").get(); + expect(row.no_parte_intercambio).toBe('NEW-INT'); + }); +}); diff --git a/tests/fixtures/sample-catalog.csv b/tests/fixtures/sample-catalog.csv new file mode 100644 index 0000000..d72a3a4 --- /dev/null +++ b/tests/fixtures/sample-catalog.csv @@ -0,0 +1,4 @@ +no_parte_proveedor,no_parte_intercambio +BK-001,INT-9999 +FT-200,INT-8888 +AM-300,INT-7777