/**
* Lulu Press Boekhouding Automatisering
*
* Dit script:
* 1. Haalt alle ongekoppelde Lulu bankboekingen op
* 2. Scant Lulu facturen (PDF) voor bedragen
* 3. Matcht op Payment Total (EUR)
* 4. Maakt inkoopboeking aan met correcte BTW
* 5. Upload document
* 6. Koppelt aan bankboeking
*/
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { SnelStartClient } = require('../dist/integrations/snelstart/client.js');
const { loadConfig } = require('../dist/config/index.js');
// Config
const FACTUREN_DIR = path.join(__dirname, '..', 'facturen', 'lulu-2025');
const DRY_RUN = process.argv.includes('--dry-run');
const LIMIT = parseInt(process.argv.find(a => a.startsWith('--limit='))?.split('=')[1] || '5');
// SnelStart IDs (VCA administratie)
const LULU_LEVERANCIER_ID = '8dd629b2-5b5e-47d2-ad02-0b37cd7b919e';
const GROOTBOEK_INKOOP_ID = '0a3f5d9e-8b2c-4a1d-9e7f-6c5b4a3d2e1f'; // 4100 Inkoop goederen - moet opgezocht worden
const GROOTBOEK_4100_NUMMER = 4100;
// Extract bedragen uit Lulu PDF
function extractLuluData(pdfPath) {
try {
const text = execSync(`pdftotext -layout "${pdfPath}" -`, { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
const data = {
jobId: null,
orderId: null,
paymentTotal: null,
itemSubtotal: null,
salesTax: null,
shippingAddress: null,
placedOn: null,
};
// Print Job ID
const jobMatch = text.match(/Print Job ID\s*[\n\r]*\s*(\d+)/);
if (jobMatch) data.jobId = jobMatch[1];
// Order ID
const orderMatch = text.match(/Order ID\s*[\n\r]*\s*(\d+)/);
if (orderMatch) data.orderId = orderMatch[1];
// Payment Total (belangrijkste - moet matchen met creditcard)
// Kan zijn: "Payment Total 20.28 EUR" of "Payment Total\n20.28 EUR"
const paymentMatch = text.match(/Payment Total\s+([\d.,]+)\s*EUR/i);
if (paymentMatch) data.paymentTotal = parseFloat(paymentMatch[1].replace(',', '.'));
// Item Subtotal (excl BTW en shipping)
const subtotalMatch = text.match(/Item Subtotal\s+([\d.,]+)\s*EUR/i);
if (subtotalMatch) data.itemSubtotal = parseFloat(subtotalMatch[1].replace(',', '.'));
// Sales Tax - laatste occurrence (onderaan de factuur)
const taxMatches = text.match(/Sales Tax\s+([\d.,]+)\s*EUR/gi);
if (taxMatches && taxMatches.length > 0) {
const lastTax = taxMatches[taxMatches.length - 1];
const taxValue = lastTax.match(/([\d.,]+)\s*EUR/i);
if (taxValue) data.salesTax = parseFloat(taxValue[1].replace(',', '.'));
}
// Placed on date
const dateMatch = text.match(/Placed on\s+(\w+\s+\d+,\s+\d{4})/);
if (dateMatch) data.placedOn = dateMatch[1];
// Shipping address (voor NL check)
if (text.includes('\nNL\n') || text.includes(' NL\n')) {
data.shippingAddress = 'NL';
}
return data;
} catch (e) {
console.error(` Error parsing ${pdfPath}: ${e.message}`);
return null;
}
}
// Zoek grootboek ID op nummer
async function getGrootboekId(client, nummer) {
const grootboeken = await client.getGrootboeken();
const gb = grootboeken.find(g => g.nummer === nummer);
return gb?.id;
}
// Main
async function main() {
console.log('='.repeat(60));
console.log('LULU PRESS BOEKHOUDING AUTOMATISERING');
console.log('='.repeat(60));
console.log(`Mode: ${DRY_RUN ? 'DRY RUN (geen wijzigingen)' : 'LIVE'}`);
console.log(`Limit: ${LIMIT} facturen`);
console.log('');
// Init SnelStart client
const config = loadConfig();
const adminConfig = config.snelstart.admins.find(a => a.id === 'vca') || config.snelstart.admins[0];
const client = new SnelStartClient(adminConfig);
// Stap 1: Haal grootboek ID op voor 4100
console.log('📚 Grootboek 4100 opzoeken...');
const grootboek4100Id = await getGrootboekId(client, GROOTBOEK_4100_NUMMER);
if (!grootboek4100Id) {
console.error('❌ Grootboek 4100 niet gevonden!');
process.exit(1);
}
console.log(` ID: ${grootboek4100Id}`);
// Stap 2: Haal ongekoppelde Lulu bankboekingen op van creditcard 1190
console.log('\n📥 Lulu bankboekingen ophalen (creditcard 1190)...');
// Haal dagboek 1190 ID op
const dagboeken = await client.request('/dagboeken', 'GET');
const ccDagboek = dagboeken.find(d => d.nummer === 1190);
if (!ccDagboek) {
console.error('❌ Creditcard dagboek 1190 niet gevonden!');
process.exit(1);
}
console.log(` Creditcard dagboek ID: ${ccDagboek.id}`);
// Haal alleen bankboekingen van creditcard 1190 op
const allBankboekingen = await client.request('/bankboekingen', 'GET', null, {
'$filter': `dagboek/id eq guid'${ccDagboek.id}' and modifiedOn ge 2025-01-01`
});
const luluBoekingen = allBankboekingen.filter(b => {
if (!b.omschrijving?.toLowerCase().includes('lulu')) return false;
// Check of al gekoppeld (heeft inkoopboekingBoekingsRegels)
if (b.inkoopboekingBoekingsRegels?.length > 0) return false;
return true;
});
console.log(` Totaal: ${luluBoekingen.length} ongekoppelde Lulu transacties`);
// Maak lookup map op bedrag
const boekingByBedrag = new Map();
for (const b of luluBoekingen) {
const bedrag = b.bedragUitgegeven || 0;
if (!boekingByBedrag.has(bedrag)) {
boekingByBedrag.set(bedrag, []);
}
boekingByBedrag.get(bedrag).push(b);
}
// Stap 3: Haal bestaande LULU facturen op om duplicates te voorkomen
console.log('\n📄 Bestaande LULU facturen ophalen...');
const bestaandeFacturen = await client.request('/inkoopfacturen', 'GET', null, {
'$filter': "factuurnummer ge 'LULU-' and factuurnummer le 'LULU-Z'"
});
const usedJobIds = new Set();
for (const f of bestaandeFacturen) {
const match = f.factuurnummer.match(/LULU-(\d+)/);
if (match) usedJobIds.add(match[1]);
}
console.log(` ${usedJobIds.size} facturen al in SnelStart`);
// Stap 4: Scan Lulu facturen
console.log('\n📄 Lulu facturen scannen...');
const pdfFiles = fs.readdirSync(FACTUREN_DIR).filter(f => f.endsWith('.pdf') && f.includes('job'));
console.log(` ${pdfFiles.length} PDF facturen gevonden`);
const matches = [];
const noMatch = [];
for (const file of pdfFiles) {
// Skip als factuur al in SnelStart staat
const jobMatch = file.match(/lulu-job-(\d+)\.pdf/);
if (jobMatch && usedJobIds.has(jobMatch[1])) {
continue; // Al geboekt, skip
}
const pdfPath = path.join(FACTUREN_DIR, file);
const data = extractLuluData(pdfPath);
if (!data || !data.paymentTotal) {
console.log(` ⚠️ ${file}: kon bedrag niet extraheren`);
continue;
}
// Zoek matching bankboeking op bedrag
const candidates = boekingByBedrag.get(data.paymentTotal) || [];
if (candidates.length > 0) {
// Pak eerste beschikbare match
const bankboeking = candidates.shift();
matches.push({
factuur: file,
pdfPath,
data,
bankboeking,
});
// Update map (verwijder gebruikte boeking)
if (candidates.length === 0) {
boekingByBedrag.delete(data.paymentTotal);
}
} else {
noMatch.push({ factuur: file, data });
}
}
console.log(`\n✅ ${matches.length} matches gevonden`);
console.log(`❌ ${noMatch.length} facturen zonder match`);
// Stap 4: Verwerk matches
console.log('\n' + '='.repeat(60));
console.log('VERWERKING');
console.log('='.repeat(60));
let processed = 0;
let errors = 0;
// Rate limiting: max 10 requests per 30 sec = 1 per 3 sec
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const DELAY_MS = 3500; // 3.5 sec between operations
for (const match of matches.slice(0, LIMIT)) {
console.log(`\n📝 ${match.factuur}`);
console.log(` Job ID: ${match.data.jobId}`);
console.log(` Payment Total: €${match.data.paymentTotal}`);
console.log(` Sales Tax: €${match.data.salesTax || 0}`);
console.log(` Bankboeking: ${match.bankboeking.datum.substring(0, 10)} - ${match.bankboeking.id}`);
if (DRY_RUN) {
console.log(' [DRY RUN] Zou verwerken...');
processed++;
continue;
}
try {
// Bereken bedragen - BELANGRIJK: afronden op 2 decimalen!
const totaal = match.data.paymentTotal;
const btw = match.data.salesTax || 0;
const exclBtw = +(totaal - btw).toFixed(2); // Afronden om floating point errors te voorkomen
// A) Maak inkoopboeking aan
console.log(' → Inkoopboeking aanmaken...');
// Uniek factuurnummer: LULU-{print_job_id} uit bestandsnaam
const printJobMatch = match.factuur.match(/lulu-job-(\d+)\.pdf/);
const printJobId = printJobMatch ? printJobMatch[1] : match.data.jobId;
const factuurnummer = `LULU-${printJobId}`;
const factuurdatum = match.bankboeking.datum.substring(0, 10);
// Bepaal BTW soort (NL levering = 21% hoog tarief)
const isNL = match.data.shippingAddress === 'NL';
const btwSoort = isNL && btw > 0 ? 'Hoog' : 'Geen';
const inkoopboekingData = {
leverancier: { id: LULU_LEVERANCIER_ID },
factuurnummer,
factuurdatum,
factuurbedrag: totaal,
boekingsregels: [{
omschrijving: `Print job ${match.data.jobId}`,
grootboek: { id: grootboek4100Id },
bedrag: exclBtw,
btwSoort: btwSoort,
}],
btw: btw > 0 ? [{
btwSoort: 'InkopenHoog',
btwBedrag: btw,
}] : [],
};
const inkoopboeking = await client.request('/inkoopboekingen', 'POST', inkoopboekingData);
console.log(` ✓ Inkoopboeking: ${inkoopboeking.id}`);
// B) Upload document
console.log(' → Document uploaden...');
const pdfContent = fs.readFileSync(match.pdfPath);
const base64Content = pdfContent.toString('base64');
await client.request('/documenten/Inkoopboekingen', 'POST', {
parentIdentifier: inkoopboeking.id,
fileName: match.factuur,
content: base64Content,
});
console.log(' ✓ Document geüpload');
// C) Koppel aan bankboeking
console.log(' → Koppelen aan bankboeking...');
// Verwijder oude bankboeking
await client.request(`/bankboekingen/${match.bankboeking.id}`, 'DELETE');
// Maak nieuwe met inkoopboeking link
const leverancierNaam = 'Lulu Press';
const newBankboeking = await client.request('/bankboekingen', 'POST', {
dagboek: { id: match.bankboeking.dagboek.id },
datum: factuurdatum,
boekstuk: match.bankboeking.boekstuk || '',
omschrijving: match.bankboeking.omschrijving,
bedragUitgegeven: match.bankboeking.bedragUitgegeven || 0,
bedragOntvangen: match.bankboeking.bedragOntvangen || 0,
grootboekBoekingsRegels: [],
inkoopboekingBoekingsRegels: [{
boekingId: { id: inkoopboeking.id },
omschrijving: leverancierNaam.toLowerCase(),
debet: totaal,
credit: 0,
}],
verkoopboekingBoekingsRegels: [],
btwBoekingsregels: [],
});
console.log(` ✓ Bankboeking gekoppeld: ${newBankboeking.id}`);
processed++;
// Rate limiting delay
await sleep(DELAY_MS);
} catch (e) {
// Bij duplicate factuurnummer, sla over (al geboekt)
if (e.message.includes('factuurnummer bestaat al')) {
console.log(` ⏭️ Al geboekt (skip)`);
processed++; // Telt als verwerkt
} else {
console.error(` ❌ Error: ${e.message}`);
errors++;
}
// Rate limiting delay ook bij errors
await sleep(DELAY_MS);
}
}
// Samenvatting
console.log('\n' + '='.repeat(60));
console.log('SAMENVATTING');
console.log('='.repeat(60));
console.log(`Verwerkt: ${processed}`);
console.log(`Errors: ${errors}`);
console.log(`Nog te doen: ${matches.length - processed}`);
if (noMatch.length > 0) {
console.log(`\nFacturen zonder match (${noMatch.length}):`);
for (const nm of noMatch.slice(0, 10)) {
console.log(` - ${nm.factuur}: €${nm.data.paymentTotal}`);
}
if (noMatch.length > 10) {
console.log(` ... en ${noMatch.length - 10} meer`);
}
}
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});