187 lines
7.4 KiB
JavaScript
187 lines
7.4 KiB
JavaScript
#!/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, '<').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('<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 d’acte {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.');
|