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 <noreply@anthropic.com>
This commit is contained in:
consultoria-as
2026-02-17 06:31:54 +00:00
parent 5da79cc3ec
commit c341726dda
4 changed files with 128 additions and 2 deletions

View File

@@ -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 };

View File

@@ -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(

View File

@@ -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');
});
});

4
tests/fixtures/sample-catalog.csv vendored Normal file
View File

@@ -0,0 +1,4 @@
no_parte_proveedor,no_parte_intercambio
BK-001,INT-9999
FT-200,INT-8888
AM-300,INT-7777
1 no_parte_proveedor no_parte_intercambio
2 BK-001 INT-9999
3 FT-200 INT-8888
4 AM-300 INT-7777