#!/usr/bin/env node /** * Reconstruit les templates Avenant Advalo pour la parité v1. * * Les .docx d'origine (lettre AXA stylée) n'ont que l'en-tête agent/client. La v1 * (golang/bordereau.go) construit par CODE : un tableau de prix (CAPITAUX | TAUX | * COTISATION | COUT D'ACTE | A PERCEVOIR), la phrase "à percevoir", et — pour le * périodique avec liste — un tableau récapitulatif des transports. * * Ce script: * 1. sauvegarde la version pristine de chaque template dans _source/ (au 1er passage); * 2. convertit les tokens bruts (nomAgent, …) en tags docxtemplater {token} (runs isolés); * 3. injecte le tableau de prix + la phrase + (Avenant) la boucle {#avecListe}/{#listeTransports}; * 4. réécrit le .docx live. Idempotent : retransforme toujours depuis _source/. * * Usage: node scripts/advalo-build-templates.js */ const fs = require('fs'); const path = require('path'); const PizZip = require('pizzip'); const TEMPLATE_DIR = path.resolve(__dirname, '..', 'src', 'templates', 'advalo'); const SOURCE_DIR = path.join(TEMPLATE_DIR, '_source'); const COMMON_TOKENS = [ 'nomAgent', 'adresseAgent', 'postalAgent', 'telAgent', 'faxAgent', 'nomClient', 'adresseClient', 'codePostal', 'numContrat', 'numClient', 'intervalle', 'numAgent' ]; const BLUE = '254E9B'; function escAttr(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } // Paragraphe simple. `text` peut contenir des tags docxtemplater {…}. function para(text, { bold = false, color = null, size = null, align = null } = {}) { const rpr = []; if (bold) rpr.push(''); if (color) rpr.push(``); if (size) rpr.push(``); const rPr = rpr.length ? `${rpr.join('')}` : ''; const pPrParts = []; if (align) pPrParts.push(``); if (rpr.length) pPrParts.push(`${rpr.join('')}`); const pPr = pPrParts.length ? `${pPrParts.join('')}` : ''; return `${pPr}${rPr}${text}`; } // Cellule de tableau. `text` peut contenir des tags docxtemplater. function cell(text, width, { bold = false, color = null, fill = null } = {}) { const rpr = []; if (bold) rpr.push(''); if (color) rpr.push(``); rpr.push(''); const rPr = `${rpr.join('')}`; const shd = fill ? `` : ''; return `${shd}` + `${rPr}${text}`; } function tableOpen(widths) { const borders = ['top', 'left', 'bottom', 'right', 'insideH', 'insideV'] .map((b) => ``) .join(''); const grid = widths.map((w) => ``).join(''); return `${borders}${grid}`; } function row(cells) { return `${cells.join('')}`; } // Tableau de prix à une ligne (parité v1: CAPITAUX | TAUX | COTISATION | COUT D'ACTE | A PERCEVOIR). function pricingTable() { const W = [1900, 1500, 1900, 1900, 1900]; const headers = ['CAPITAUX', 'TAUX', 'COTISATION', "COUT D'ACTE", 'A PERCEVOIR']; const headerRow = row(headers.map((h, i) => cell(h, W[i], { bold: true, color: 'FFFFFF', fill: BLUE }))); const dataRow = row([ cell('{capitauxField} €', W[0]), cell('{tauxField} %', W[1]), cell('{cotisationField} €', W[2]), cell('{coutActeField} €', W[3]), cell('{cotisationTTC} €', W[4]) ]); return tableOpen(W) + headerRow + dataRow + ''; } // Tableau récapitulatif des transports (boucle docxtemplater sur {listeTransports}). function recapTable() { const W = [1300, 1300, 1300, 1300, 1300, 1500, 1100]; const headers = ['N° Demande', 'Date demande', 'Mode', 'Capital', 'Départ', 'Arrivée', 'Transport', 'Tarif']; const W8 = [1150, 1150, 1100, 1150, 1100, 1100, 1300, 1000]; const headerRow = row(headers.map((h, i) => cell(h, W8[i], { bold: true, color: 'FFFFFF', fill: BLUE }))); const loopRow = row([ cell('{#listeTransports}{numDemande}', W8[0]), cell('{dateDemande}', W8[1]), cell('{mode}', W8[2]), cell('{capital} €', W8[3]), cell('{depart}', W8[4]), cell('{arrivee}', W8[5]), cell('{dateTransport}', W8[6]), cell('{tarif} €{/listeTransports}', W8[7]) ]); return tableOpen(W8) + headerRow + loopRow + ''; } const EMPTY_P = ''; function buildAvenantBlock() { // Tableau prix + phrase "à percevoir" + récap conditionnel (parité v1 remplissageAvenant). return [ EMPTY_P, pricingTable(), EMPTY_P, para('La cotisation à percevoir à la signature du présent avenant est de {cotisationTTC} €.', { bold: true }), EMPTY_P, para('{#avecListe}', {}), para("RECAPITULATIF DES TRANSPORTS SAISIS DANS L'OUTIL AD VALOREM", { bold: true, color: BLUE }), recapTable(), para('{/avecListe}', {}), EMPTY_P ].join(''); } function buildPonctuelBlock() { // Détails transport + tableau prix + phrase "Il est perçu" (parité v1 remplissageAvenantPonctuel). return [ EMPTY_P, para('Transport assuré du {dateDebut} au {dateFin}.'), para('Nature de la marchandise : {typeMarchandise}.'), para('Trajet : {depart} → {arrivee}.'), para('Mode(s) de transport : {modes}.'), EMPTY_P, pricingTable(), EMPTY_P, para('Il est perçu la somme de {cotisationTTC} € TTC ' + '(dont cotisation HT {cotisationField} € et coût d’acte {coutActeField} €).', { bold: true }), EMPTY_P ].join(''); } function convertTokens(xml, tokens) { let out = xml; tokens.forEach((tok) => { out = out.replace(new RegExp(`(]*>)${tok}()`, 'g'), `$1{${tok}}$2`); }); return out; } function insertBeforeSectPr(xml, block) { const idx = xml.lastIndexOf('', `${block}`); } return xml.slice(0, idx) + block + xml.slice(idx); } function rebuild(name, blockBuilder, { stripCapitauxAnchor = false } = {}) { const livePath = path.join(TEMPLATE_DIR, `${name}.docx`); const sourcePath = path.join(SOURCE_DIR, `${name}.docx`); fs.mkdirSync(SOURCE_DIR, { recursive: true }); if (!fs.existsSync(sourcePath)) { fs.copyFileSync(livePath, sourcePath); console.log(`[source] sauvegarde pristine -> ${path.relative(process.cwd(), sourcePath)}`); } const zip = new PizZip(fs.readFileSync(sourcePath)); let xml = zip.file('word/document.xml').asText(); xml = convertTokens(xml, COMMON_TOKENS); if (stripCapitauxAnchor) { // L'ancre inline `capitauxField` de la v1 est remplacée par le vrai tableau (ci-dessous). xml = xml.replace(/(]*>)capitauxField(<\/w:t>)/g, '$1$2'); } xml = insertBeforeSectPr(xml, blockBuilder()); zip.file('word/document.xml', xml); fs.writeFileSync(livePath, zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' })); console.log(`[build] ${name}.docx reconstruit (${xml.length} octets de document.xml)`); } rebuild('Avenant', buildAvenantBlock, { stripCapitauxAnchor: true }); rebuild('Avenant_Ponctuel', buildPonctuelBlock); console.log('Templates Avenant reconstruits.');