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:
54
src/services/catalog-service.js
Normal file
54
src/services/catalog-service.js
Normal 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 };
|
||||
@@ -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(
|
||||
|
||||
68
tests/catalog-service.test.js
Normal file
68
tests/catalog-service.test.js
Normal 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
4
tests/fixtures/sample-catalog.csv
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
no_parte_proveedor,no_parte_intercambio
|
||||
BK-001,INT-9999
|
||||
FT-200,INT-8888
|
||||
AM-300,INT-7777
|
||||
|
Reference in New Issue
Block a user