Ajoute la TVA (%) et calcule automatiquement le Montant TTC

Architecture : Montant HT reste saisi à la main, un nouveau champ TVA
(%) le complète, et Montant TTC = round(HT * (1 + TVA/100), 2) devient
une valeur calculée plutôt que saisie -- champ readonly dans le
formulaire (aperçu live en JS), calcul faisant foi côté serveur dans
figli_compta_ledger_node_presave().

Migration (update hooks 8004/8005) : Montant TTC n'est **jamais**
modifié pour les données historiques -- seul un TVA effectif est
rétro-calculé depuis HT/TTC existants et ajouté en tant que nouvelle
métadonnée (1498 lignes renseignées, 58 laissées vides faute de TTC
source). Précision du champ TVA fixée à 4 décimales après vérification
empirique sur les 1556 lignes existantes : reconstruire TTC = HT * (1 +
TVA/100) avec un taux arrondi à 4 décimales ne s'écarte du TTC réel que
pour 9 lignes (probables factures multi-taux), contre 74 à 2 décimales.

Garde-fou supplémentaire dans le presave : le recalcul du TTC ne se
déclenche que si HT ou TVA ont réellement changé par rapport à la
révision précédente (comparaison à $node->original) -- sans ça,
rouvrir une ancienne ligne migrée pour corriger un simple libellé
aurait silencieusement dérivé son TTC historique de ±0,01€ à cause de
l'arrondi du taux rétro-calculé, ce qu'interdit la règle du projet de
ne jamais corriger les données historiques.

Réorganisation du formulaire : N° Facture rejoint Client sur une même
ligne, Montant HT/TVA/Montant TTC forment la ligne suivante -- les
poids de champs doivent rester des entiers (Drupal tronque silencieusement
tout poids fractionnaire lors de la sauvegarde de l'affichage).
This commit is contained in:
2026-09-07 11:36:53 +02:00
parent f00f680143
commit b7490eb76a
9 changed files with 354 additions and 29 deletions
@@ -12,6 +12,7 @@ dependencies:
- field.field.node.ligne_comptable.field_notes - field.field.node.ligne_comptable.field_notes
- field.field.node.ligne_comptable.field_numero_facture - field.field.node.ligne_comptable.field_numero_facture
- field.field.node.ligne_comptable.field_repartition - field.field.node.ligne_comptable.field_repartition
- field.field.node.ligne_comptable.field_tva
- field.field.node.ligne_comptable.field_type_ligne - field.field.node.ligne_comptable.field_type_ligne
- node.type.ligne_comptable - node.type.ligne_comptable
module: module:
@@ -47,7 +48,7 @@ content:
third_party_settings: { } third_party_settings: { }
field_entree_liee: field_entree_liee:
type: entity_reference_autocomplete type: entity_reference_autocomplete
weight: 2 weight: 4
region: content region: content
settings: settings:
match_operator: CONTAINS match_operator: CONTAINS
@@ -57,7 +58,7 @@ content:
third_party_settings: { } third_party_settings: { }
field_flag: field_flag:
type: entity_reference_autocomplete_tags type: entity_reference_autocomplete_tags
weight: 8 weight: 10
region: content region: content
settings: settings:
match_operator: CONTAINS match_operator: CONTAINS
@@ -67,21 +68,21 @@ content:
third_party_settings: { } third_party_settings: { }
field_montant_ht: field_montant_ht:
type: number type: number
weight: 3 weight: 5
region: content region: content
settings: settings:
placeholder: '' placeholder: ''
third_party_settings: { } third_party_settings: { }
field_montant_ttc: field_montant_ttc:
type: number type: number
weight: 4 weight: 7
region: content region: content
settings: settings:
placeholder: '' placeholder: ''
third_party_settings: { } third_party_settings: { }
field_notes: field_notes:
type: string_textarea type: string_textarea
weight: 6 weight: 9
region: content region: content
settings: settings:
rows: 5 rows: 5
@@ -97,7 +98,7 @@ content:
third_party_settings: { } third_party_settings: { }
field_repartition: field_repartition:
type: paragraphs type: paragraphs
weight: 5 weight: 8
region: content region: content
settings: settings:
title: Répartition title: Répartition
@@ -113,6 +114,13 @@ content:
collapse_edit_all: collapse_edit_all collapse_edit_all: collapse_edit_all
duplicate: duplicate duplicate: duplicate
third_party_settings: { } third_party_settings: { }
field_tva:
type: number
weight: 6
region: content
settings:
placeholder: ''
third_party_settings: { }
field_type_ligne: field_type_ligne:
type: options_select type: options_select
weight: 1 weight: 1
@@ -12,6 +12,7 @@ dependencies:
- field.field.node.ligne_comptable.field_notes - field.field.node.ligne_comptable.field_notes
- field.field.node.ligne_comptable.field_numero_facture - field.field.node.ligne_comptable.field_numero_facture
- field.field.node.ligne_comptable.field_repartition - field.field.node.ligne_comptable.field_repartition
- field.field.node.ligne_comptable.field_tva
- field.field.node.ligne_comptable.field_type_ligne - field.field.node.ligne_comptable.field_type_ligne
- node.type.ligne_comptable - node.type.ligne_comptable
module: module:
@@ -55,7 +56,7 @@ content:
settings: settings:
link: true link: true
third_party_settings: { } third_party_settings: { }
weight: 8 weight: 10
region: content region: content
field_montant_ht: field_montant_ht:
type: number_decimal type: number_decimal
@@ -66,7 +67,7 @@ content:
scale: 2 scale: 2
prefix_suffix: true prefix_suffix: true
third_party_settings: { } third_party_settings: { }
weight: 3 weight: 5
region: content region: content
field_montant_ttc: field_montant_ttc:
type: number_decimal type: number_decimal
@@ -77,14 +78,14 @@ content:
scale: 2 scale: 2
prefix_suffix: true prefix_suffix: true
third_party_settings: { } third_party_settings: { }
weight: 4 weight: 7
region: content region: content
field_notes: field_notes:
type: basic_string type: basic_string
label: above label: above
settings: { } settings: { }
third_party_settings: { } third_party_settings: { }
weight: 6 weight: 9
region: content region: content
field_numero_facture: field_numero_facture:
type: string type: string
@@ -102,7 +103,18 @@ content:
view_mode: default view_mode: default
link: '' link: ''
third_party_settings: { } third_party_settings: { }
weight: 5 weight: 8
region: content
field_tva:
type: number_decimal
label: above
settings:
thousand_separator: ''
decimal_separator: .
scale: 2
prefix_suffix: true
third_party_settings: { }
weight: 6
region: content region: content
field_type_ligne: field_type_ligne:
type: list_default type: list_default
@@ -0,0 +1,25 @@
uuid: 9e9fc15c-9f7b-466a-bab0-fc6fe802a22f
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_tva
- node.type.ligne_comptable
id: node.ligne_comptable.field_tva
field_name: field_tva
entity_type: node
bundle: ligne_comptable
label: 'TVA (%)'
description: ''
required: false
translatable: true
default_value:
-
value: 0.0
default_value_callback: ''
settings:
min: null
max: null
prefix: ''
suffix: ''
field_type: decimal
@@ -0,0 +1,20 @@
uuid: 92ad1651-ab19-4c60-ad8c-4824fd97ecc3
langcode: en
status: true
dependencies:
module:
- node
id: node.field_tva
field_name: field_tva
entity_type: node
type: decimal
settings:
precision: 8
scale: 4
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
@@ -59,12 +59,14 @@ html.gin--dark-mode .figli-ledger-form {
display: none; display: none;
} }
/* Row layout. Everything not listed here (Montant HT / N° Facture / /* Row layout, fully explicit (not left to grid auto-placement) --
Montant TTC) is left to grid auto-placement, which -- given they're there are now two separate 1-col-plus-2-col row groupings (Client +
the only three single-column items appearing consecutively in DOM N° Facture, then Montant HT + TVA + Montant TTC) and relying on DOM
order -- lines them up as one row of three for free. */ order alone to keep them from bleeding into each other got fragile
once a 4th financial field (TVA) entered the picture. This must stay
in sync with the field weights set in
figli_compta_ledger_update_8004()/_figli_compta_ledger_create_node_type_ligne_comptable(). */
.figli-ledger-form > .field--name-title, .figli-ledger-form > .field--name-title,
.figli-ledger-form > .field--name-field-client,
.figli-ledger-form > .field--name-field-entree-liee, .figli-ledger-form > .field--name-field-entree-liee,
.figli-ledger-form > .field--name-field-repartition, .figli-ledger-form > .field--name-field-repartition,
.figli-ledger-form > .field--name-field-notes, .figli-ledger-form > .field--name-field-notes,
@@ -77,6 +79,21 @@ html.gin--dark-mode .figli-ledger-form {
.figli-ledger-form > .field--name-field-type-ligne { .figli-ledger-form > .field--name-field-type-ligne {
grid-column: 2 / 4; grid-column: 2 / 4;
} }
.figli-ledger-form > .field--name-field-client {
grid-column: 1 / 3;
}
.figli-ledger-form > .field--name-field-numero-facture {
grid-column: 3 / 4;
}
.figli-ledger-form > .field--name-field-montant-ht {
grid-column: 1 / 2;
}
.figli-ledger-form > .field--name-field-tva {
grid-column: 2 / 3;
}
.figli-ledger-form > .field--name-field-montant-ttc {
grid-column: 3 / 4;
}
/* Field basics */ /* Field basics */
.figli-ledger-form .form-item__label { .figli-ledger-form .form-item__label {
@@ -118,6 +135,14 @@ html.gin--dark-mode .figli-ledger-form {
outline: 2px solid var(--flform-accent); outline: 2px solid var(--flform-accent);
outline-offset: 1px; outline-offset: 1px;
} }
/* Montant TTC: readonly, computed from HT + TVA (see
figli_compta_ledger_node_presave()) -- a faint fill instead of the
plain white/transparent of an editable field reads as "there but not
for you to type in" without looking disabled/greyed-out. */
.figli-ledger-form input[readonly].form-element {
background: var(--flform-bg-subtle);
cursor: default;
}
.figli-ledger-form input[type="date"].form-element { .figli-ledger-form input[type="date"].form-element {
width: auto; width: auto;
min-width: 9.5rem; min-width: 9.5rem;
@@ -211,6 +211,13 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
_figli_field('node', 'ligne_comptable', 'field_numero_facture', 'N° Facture', 'string', ['max_length' => 64]); _figli_field('node', 'ligne_comptable', 'field_numero_facture', 'N° Facture', 'string', ['max_length' => 64]);
_figli_field('node', 'ligne_comptable', 'field_montant_ht', 'Montant HT (€)', 'decimal', ['precision' => 12, 'scale' => 2], TRUE); _figli_field('node', 'ligne_comptable', 'field_montant_ht', 'Montant HT (€)', 'decimal', ['precision' => 12, 'scale' => 2], TRUE);
// Scale 4 (not 2) so that reconstructing an existing invoice's exact
// TTC from HT * (1 + TVA/100) round-trips to the centime for the
// overwhelming majority of migrated lines -- see
// figli_compta_ledger_update_8005()'s docblock for the empirical check
// behind that choice. A user typing a plain rate like "20" is
// unaffected either way.
_figli_field('node', 'ligne_comptable', 'field_tva', 'TVA (%)', 'decimal', ['precision' => 8, 'scale' => 4, 'min' => 0]);
_figli_field('node', 'ligne_comptable', 'field_montant_ttc', 'Montant TTC (€)', 'decimal', ['precision' => 12, 'scale' => 2]); _figli_field('node', 'ligne_comptable', 'field_montant_ttc', 'Montant TTC (€)', 'decimal', ['precision' => 12, 'scale' => 2]);
_figli_field('node', 'ligne_comptable', 'field_notes', 'Notes / détail', 'string_long'); _figli_field('node', 'ligne_comptable', 'field_notes', 'Notes / détail', 'string_long');
@@ -235,11 +242,12 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
->setComponent('field_type_ligne', ['type' => 'options_select', 'weight' => 1]) ->setComponent('field_type_ligne', ['type' => 'options_select', 'weight' => 1])
->setComponent('field_client', ['type' => 'entity_reference_autocomplete', 'weight' => 2]) ->setComponent('field_client', ['type' => 'entity_reference_autocomplete', 'weight' => 2])
->setComponent('field_numero_facture', ['type' => 'string_textfield', 'weight' => 3]) ->setComponent('field_numero_facture', ['type' => 'string_textfield', 'weight' => 3])
->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 4]) ->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 5])
->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 5]) ->setComponent('field_tva', ['type' => 'number', 'weight' => 6])
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 6, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']]) ->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 7])
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 7]) ->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 8, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 8]) ->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 9])
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 10])
->save(); ->save();
} }
@@ -254,11 +262,12 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
->setComponent('field_type_ligne', ['type' => 'list_default', 'weight' => 1]) ->setComponent('field_type_ligne', ['type' => 'list_default', 'weight' => 1])
->setComponent('field_client', ['type' => 'entity_reference_label', 'weight' => 2]) ->setComponent('field_client', ['type' => 'entity_reference_label', 'weight' => 2])
->setComponent('field_numero_facture', ['type' => 'string', 'weight' => 3]) ->setComponent('field_numero_facture', ['type' => 'string', 'weight' => 3])
->setComponent('field_montant_ht', ['type' => 'number_decimal', 'weight' => 4]) ->setComponent('field_montant_ht', ['type' => 'number_decimal', 'weight' => 5])
->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 5]) ->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 6])
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 6]) ->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 7])
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 7]) ->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 8])
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 8]) ->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 9])
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 10])
->save(); ->save();
} }
} }
@@ -353,3 +362,122 @@ function figli_compta_ledger_update_8003() {
$view_display->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 8])->save(); $view_display->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 8])->save();
} }
} }
/**
* Adds field_tva (TVA %) to ligne_comptable and switches Montant TTC from
* manual entry to an automatically computed value (see
* figli_compta_ledger_node_presave(): TTC = round(HT * (1 + TVA/100), 2)
* from now on, whenever the line isn't a skip_validation-flagged
* migration save). Existing content is backfilled separately, see
* figli_compta_ledger_update_8005().
*
* Also reweights field_numero_facture (now next to Client, both
* identifying "which invoice/client" this line is about) and
* field_entree_liee (pushed just after it) so the HT/TVA/TTC trio can
* form its own clean three-column row afterwards -- see
* css/ledger-form.css's explicit grid-column rules, which this weight
* order must stay in sync with.
*/
function figli_compta_ledger_update_8004() {
_figli_field('node', 'ligne_comptable', 'field_tva', 'TVA (%)', 'decimal', ['precision' => 8, 'scale' => 4, 'min' => 0]);
// New lines default to 0% rather than blank -- the common case (every
// type other than charge/entrée/achat/hébergement with a real invoice)
// has no VAT at all, so this is one less field associates need to
// touch for most lines.
$field_tva = FieldConfig::loadByName('node', 'ligne_comptable', 'field_tva');
if ($field_tva && $field_tva->getDefaultValueLiteral() === []) {
$field_tva->setDefaultValue(0)->save();
}
// Weights are plain integers -- entity_display component config
// schema coerces anything else (a fractional weight silently rounds/
// truncates to its integer part on save), so the row-grouping can't be
// expressed as "insert field_tva between HT and TTC" via a fractional
// weight the way it might be elsewhere; every field in this block gets
// an explicit new integer instead.
$form_display = EntityFormDisplay::load('node.ligne_comptable.default');
if ($form_display) {
$form_display->setComponent('field_numero_facture', ['type' => 'string_textfield', 'weight' => 3]);
$form_display->setComponent('field_entree_liee', ['type' => 'entity_reference_autocomplete', 'weight' => 4] + (array) $form_display->getComponent('field_entree_liee'));
$form_display->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 5] + (array) $form_display->getComponent('field_montant_ht'));
$form_display->setComponent('field_tva', ['type' => 'number', 'weight' => 6] + (array) $form_display->getComponent('field_tva'));
$form_display->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 7] + (array) $form_display->getComponent('field_montant_ttc'));
$form_display->setComponent('field_repartition', ['weight' => 8] + (array) $form_display->getComponent('field_repartition'));
$form_display->setComponent('field_notes', ['weight' => 9] + (array) $form_display->getComponent('field_notes'));
$form_display->setComponent('field_flag', ['weight' => 10] + (array) $form_display->getComponent('field_flag'));
$form_display->save();
}
$view_display = EntityViewDisplay::load('node.ligne_comptable.default');
if ($view_display) {
$view_display->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 6] + (array) $view_display->getComponent('field_tva'));
$view_display->setComponent('field_numero_facture', ['weight' => 3] + (array) $view_display->getComponent('field_numero_facture'));
$view_display->setComponent('field_montant_ht', ['weight' => 5] + (array) $view_display->getComponent('field_montant_ht'));
$view_display->setComponent('field_montant_ttc', ['weight' => 7] + (array) $view_display->getComponent('field_montant_ttc'));
$view_display->setComponent('field_repartition', ['weight' => 8] + (array) $view_display->getComponent('field_repartition'));
$view_display->setComponent('field_notes', ['weight' => 9] + (array) $view_display->getComponent('field_notes'));
$view_display->setComponent('field_flag', ['weight' => 10] + (array) $view_display->getComponent('field_flag'));
$view_display->save();
}
}
/**
* Backfills field_tva for existing ligne_comptable content from the
* existing Montant HT / Montant TTC pair -- never touches Montant TTC
* itself, so every historical value stays exactly as migrated (same
* "never correct historical data" rule as everywhere else in this
* module). Left blank where there's nothing to derive from (no TTC ever
* recorded, or HT is zero).
*
* Why scale 4 on field_tva: checked empirically against all 1556
* existing lignes_comptables before picking it. Of the 946 lines with a
* real (non-zero, HT != TTC) rate, rounding the derived rate to 2
* decimals and reconstructing TTC = HT * (1 + rate/100) mismatched the
* real historical TTC (by more than a centime) for 74 of them --
* unacceptably lossy for what's supposed to be a faithful backfill. At 4
* decimals that drops to 9 (likely genuine blended/multi-rate invoices
* collapsed into a single ligne_comptable, where no single "TVA %" can
* be perfectly exact) -- an acceptable residual, and still purely
* informational since Montant TTC itself is left untouched here either
* way.
*/
function figli_compta_ledger_update_8005() {
$storage = \Drupal::entityTypeManager()->getStorage('node');
$nids = $storage->getQuery()
->accessCheck(FALSE)
->condition('type', 'ligne_comptable')
->execute();
$filled = 0;
$skipped = 0;
// Only field_tva is written here -- montant_ttc is read but never
// set, so this can never trip figli_compta_ledger_node_presave()'s
// sum(répartition) == montant_ht check either way. skip_validation is
// set anyway (a) for consistency with every other bulk-migration
// script in this module and (b) because it also now suppresses the
// TTC auto-computation added in the same presave function, which
// would otherwise silently overwrite the untouched historical TTC the
// moment this script calls save().
\Drupal::state()->set('figli_compta_ledger.skip_validation', TRUE);
foreach ($storage->loadMultiple($nids) as $node) {
if (!$node->hasField('field_montant_ht') || $node->get('field_montant_ht')->isEmpty()
|| !$node->hasField('field_montant_ttc') || $node->get('field_montant_ttc')->isEmpty()) {
$skipped++;
continue;
}
$ht = (float) $node->get('field_montant_ht')->value;
$ttc = (float) $node->get('field_montant_ttc')->value;
if (abs($ht) < 0.0001) {
$skipped++;
continue;
}
$rate = round(($ttc / $ht - 1) * 100, 4);
$node->set('field_tva', $rate);
$node->save();
$filled++;
}
\Drupal::state()->delete('figli_compta_ledger.skip_validation');
return "TVA calculée pour $filled lignes, $skipped laissées vides (pas de Montant TTC renseigné ou Montant HT nul).";
}
@@ -36,11 +36,14 @@ dashboard_compte:
- figli_compta_ledger/vue - figli_compta_ledger/vue
ledger_form: ledger_form:
js:
js/ledger-form.js: {}
css: css:
theme: theme:
css/ledger-form.css: {} css/ledger-form.css: {}
dependencies: dependencies:
- core/drupal - core/drupal
- core/once
admin_chrome: admin_chrome:
css: css:
@@ -106,6 +106,17 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
$form['status']['#access'] = FALSE; $form['status']['#access'] = FALSE;
} }
// Montant TTC is computed from Montant HT + TVA (see
// figli_compta_ledger_node_presave()) -- readonly rather than #disabled
// so js/ledger-form.js can still show a live preview as the associate
// types, and so the field still visually reads as "there but not for
// you to edit" instead of vanishing. The actual value saved is always
// recomputed server-side regardless of what this input holds.
if (isset($form['field_montant_ttc']['widget'][0]['value'])) {
$form['field_montant_ttc']['widget'][0]['value']['#attributes']['readonly'] = 'readonly';
$form['field_montant_ttc']['widget'][0]['value']['#description'] = t('Calculé automatiquement à partir du montant HT et de la TVA.');
}
$form['#attached']['library'][] = 'figli_compta_ledger/ledger_form'; $form['#attached']['library'][] = 'figli_compta_ledger/ledger_form';
$request = \Drupal::request(); $request = \Drupal::request();
@@ -200,8 +211,9 @@ function figli_compta_ledger_node_form_ajax_submit(array $form, FormStateInterfa
* source spreadsheets' raw data, including known répartition mismatches -- * source spreadsheets' raw data, including known répartition mismatches --
* those get surfaced as visible inconsistencies in the dashboard instead of * those get surfaced as visible inconsistencies in the dashboard instead of
* being silently fixed. Set the 'figli_compta_ledger.skip_validation' state * being silently fixed. Set the 'figli_compta_ledger.skip_validation' state
* flag around such a bulk import to bypass this check; new lines entered by * flag around such a bulk import to bypass this check *and* the Montant TTC
* associates through the form are never exempted. * auto-computation below; new lines entered by associates through the form
* are never exempted from either.
*/ */
function figli_compta_ledger_node_presave(NodeInterface $node) { function figli_compta_ledger_node_presave(NodeInterface $node) {
if ($node->bundle() !== 'ligne_comptable') { if ($node->bundle() !== 'ligne_comptable') {
@@ -221,7 +233,7 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
// silently mislabeling every edit with its predecessor's save time. // silently mislabeling every edit with its predecessor's save time.
$node->setRevisionCreationTime(\Drupal::time()->getRequestTime()); $node->setRevisionCreationTime(\Drupal::time()->getRequestTime());
if (!$node->hasField('field_montant_ht') || !$node->hasField('field_repartition')) { if (!$node->hasField('field_montant_ht')) {
return; return;
} }
if (\Drupal::state()->get('figli_compta_ledger.skip_validation', FALSE)) { if (\Drupal::state()->get('figli_compta_ledger.skip_validation', FALSE)) {
@@ -229,6 +241,38 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
} }
$montant_ht = (float) $node->get('field_montant_ht')->value; $montant_ht = (float) $node->get('field_montant_ht')->value;
$tva = $node->hasField('field_tva') && !$node->get('field_tva')->isEmpty() ? (float) $node->get('field_tva')->value : 0.0;
// Montant TTC is no longer entered by hand -- it's a pure function of
// Montant HT and TVA (see figli_compta_ledger_update_8004()). Only
// recompute it when HT or TVA actually changed, though, rather than on
// every save unconditionally: the TVA backfilled onto pre-migration
// lines (figli_compta_ledger_update_8005()) is a best-effort
// reconstruction and doesn't reproduce every historical Montant TTC to
// the exact centime, so blindly recomputing on an unrelated edit
// (fixing a typo in Libellé, say) would silently nudge an untouched
// historical value -- exactly what this module's "never correct
// historical data" rule exists to prevent. A genuinely new/changed
// HT or TVA has no such history to protect.
if ($node->hasField('field_montant_ttc')) {
$ht_or_tva_changed = $node->isNew();
if (!$ht_or_tva_changed && isset($node->original)) {
$original = $node->original;
$orig_ht = $original->hasField('field_montant_ht') && !$original->get('field_montant_ht')->isEmpty()
? (float) $original->get('field_montant_ht')->value : NULL;
$orig_tva = $original->hasField('field_tva') && !$original->get('field_tva')->isEmpty()
? (float) $original->get('field_tva')->value : 0.0;
$ht_or_tva_changed = $orig_ht !== $montant_ht || abs($orig_tva - $tva) > 0.00005;
}
if ($ht_or_tva_changed) {
$node->set('field_montant_ttc', round($montant_ht * (1 + $tva / 100), 2));
}
}
if (!$node->hasField('field_repartition')) {
return;
}
$somme = 0.0; $somme = 0.0;
foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) { foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) {
if ($paragraph->hasField('field_montant') && !$paragraph->get('field_montant')->isEmpty()) { if ($paragraph->hasField('field_montant') && !$paragraph->get('field_montant')->isEmpty()) {
@@ -0,0 +1,60 @@
/**
* @file
* Live preview of Montant TTC = Montant HT * (1 + TVA/100) on the ligne
* comptable add/edit form -- purely cosmetic, so the associate sees the
* computed amount before saving instead of a blank/stale readonly field.
* The authoritative computation is server-side, in
* figli_compta_ledger_node_presave() (figli_compta_ledger.module) --
* whatever ends up in this input on submit is discarded and recomputed
* there regardless of whether this script even ran.
*/
(function (Drupal, once) {
'use strict';
Drupal.behaviors.figliLedgerTvaCalc = {
attach: function (context) {
// once()'s selector match runs through context.querySelectorAll(),
// which -- like any querySelectorAll -- never matches the context
// node itself, only its descendants. The AJAX dialog opening this
// form passes the <form class="figli-ledger-form"> element itself
// as context, so a selector of '.figli-ledger-form' silently
// matched nothing and this behavior never ran. Selecting the HT
// input directly sidesteps that: it's always a genuine descendant
// of whatever context gets passed (document on a full page load,
// the form itself from the dialog, or anything in between).
once('figli-ledger-tva-calc', '[name="field_montant_ht[0][value]"]', context).forEach(function (ht) {
var form = ht.closest('form');
var tva = form.querySelector('[name="field_tva[0][value]"]');
var ttc = form.querySelector('[name="field_montant_ttc[0][value]"]');
if (!tva || !ttc) {
return;
}
function recompute() {
var htValue = parseFloat(ht.value);
if (isNaN(htValue)) {
return;
}
var tvaValue = parseFloat(tva.value);
if (isNaN(tvaValue)) {
tvaValue = 0;
}
ttc.value = (htValue * (1 + tvaValue / 100)).toFixed(2);
}
// Deliberately not called once up front on an existing (edit)
// line: the stored Montant TTC may predate this computed-field
// architecture and not be perfectly reproducible from HT/TVA to
// the centime (see figli_compta_ledger_update_8005()'s
// docblock) -- showing a recomputed number the instant the
// modal opens, before the associate has touched anything, would
// misrepresent what's actually saved. Only recompute once HT or
// TVA are actually edited, matching the same "only overwrite
// Montant TTC when HT/TVA actually changed" guard in
// figli_compta_ledger_node_presave().
ht.addEventListener('input', recompute);
tva.addEventListener('input', recompute);
});
}
};
})(Drupal, once);