personnal/ecole/scripts/advalo-build-templates.js

187 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 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('<w:b/>');
if (color) rpr.push(`<w:color w:val="${color}"/>`);
if (size) rpr.push(`<w:sz w:val="${size}"/><w:szCs w:val="${size}"/>`);
const rPr = rpr.length ? `<w:rPr>${rpr.join('')}</w:rPr>` : '';
const pPrParts = [];
if (align) pPrParts.push(`<w:jc w:val="${align}"/>`);
if (rpr.length) pPrParts.push(`<w:rPr>${rpr.join('')}</w:rPr>`);
const pPr = pPrParts.length ? `<w:pPr>${pPrParts.join('')}</w:pPr>` : '';
return `<w:p>${pPr}<w:r>${rPr}<w:t xml:space="preserve">${text}</w:t></w:r></w:p>`;
}
// 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('<w:b/>');
if (color) rpr.push(`<w:color w:val="${color}"/>`);
rpr.push('<w:sz w:val="18"/><w:szCs w:val="18"/>');
const rPr = `<w:rPr>${rpr.join('')}</w:rPr>`;
const shd = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : '';
return `<w:tc><w:tcPr><w:tcW w:w="${width}" w:type="dxa"/>${shd}<w:vAlign w:val="center"/></w:tcPr>`
+ `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r>${rPr}<w:t xml:space="preserve">${text}</w:t></w:r></w:p></w:tc>`;
}
function tableOpen(widths) {
const borders = ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']
.map((b) => `<w:${b} w:val="single" w:sz="4" w:space="0" w:color="auto"/>`)
.join('');
const grid = widths.map((w) => `<w:gridCol w:w="${w}"/>`).join('');
return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders>${borders}</w:tblBorders></w:tblPr><w:tblGrid>${grid}</w:tblGrid>`;
}
function row(cells) {
return `<w:tr>${cells.join('')}</w:tr>`;
}
// 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 + '</w:tbl>';
}
// 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 + '</w:tbl>';
}
const EMPTY_P = '<w: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 dacte {coutActeField} €).', { bold: true }),
EMPTY_P
].join('');
}
function convertTokens(xml, tokens) {
let out = xml;
tokens.forEach((tok) => {
out = out.replace(new RegExp(`(<w:t[^>]*>)${tok}(</w:t>)`, 'g'), `$1{${tok}}$2`);
});
return out;
}
function insertBeforeSectPr(xml, block) {
const idx = xml.lastIndexOf('<w:sectPr');
if (idx === -1) {
return xml.replace('</w:body>', `${block}</w:body>`);
}
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(/(<w:t[^>]*>)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.');