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:
@@ -12,6 +12,7 @@ dependencies:
|
||||
- field.field.node.ligne_comptable.field_notes
|
||||
- field.field.node.ligne_comptable.field_numero_facture
|
||||
- field.field.node.ligne_comptable.field_repartition
|
||||
- field.field.node.ligne_comptable.field_tva
|
||||
- field.field.node.ligne_comptable.field_type_ligne
|
||||
- node.type.ligne_comptable
|
||||
module:
|
||||
@@ -47,7 +48,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_entree_liee:
|
||||
type: entity_reference_autocomplete
|
||||
weight: 2
|
||||
weight: 4
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
@@ -57,7 +58,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_flag:
|
||||
type: entity_reference_autocomplete_tags
|
||||
weight: 8
|
||||
weight: 10
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
@@ -67,21 +68,21 @@ content:
|
||||
third_party_settings: { }
|
||||
field_montant_ht:
|
||||
type: number
|
||||
weight: 3
|
||||
weight: 5
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_montant_ttc:
|
||||
type: number
|
||||
weight: 4
|
||||
weight: 7
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_notes:
|
||||
type: string_textarea
|
||||
weight: 6
|
||||
weight: 9
|
||||
region: content
|
||||
settings:
|
||||
rows: 5
|
||||
@@ -97,7 +98,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_repartition:
|
||||
type: paragraphs
|
||||
weight: 5
|
||||
weight: 8
|
||||
region: content
|
||||
settings:
|
||||
title: Répartition
|
||||
@@ -113,6 +114,13 @@ content:
|
||||
collapse_edit_all: collapse_edit_all
|
||||
duplicate: duplicate
|
||||
third_party_settings: { }
|
||||
field_tva:
|
||||
type: number
|
||||
weight: 6
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_type_ligne:
|
||||
type: options_select
|
||||
weight: 1
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies:
|
||||
- field.field.node.ligne_comptable.field_notes
|
||||
- field.field.node.ligne_comptable.field_numero_facture
|
||||
- field.field.node.ligne_comptable.field_repartition
|
||||
- field.field.node.ligne_comptable.field_tva
|
||||
- field.field.node.ligne_comptable.field_type_ligne
|
||||
- node.type.ligne_comptable
|
||||
module:
|
||||
@@ -55,7 +56,7 @@ content:
|
||||
settings:
|
||||
link: true
|
||||
third_party_settings: { }
|
||||
weight: 8
|
||||
weight: 10
|
||||
region: content
|
||||
field_montant_ht:
|
||||
type: number_decimal
|
||||
@@ -66,7 +67,7 @@ content:
|
||||
scale: 2
|
||||
prefix_suffix: true
|
||||
third_party_settings: { }
|
||||
weight: 3
|
||||
weight: 5
|
||||
region: content
|
||||
field_montant_ttc:
|
||||
type: number_decimal
|
||||
@@ -77,14 +78,14 @@ content:
|
||||
scale: 2
|
||||
prefix_suffix: true
|
||||
third_party_settings: { }
|
||||
weight: 4
|
||||
weight: 7
|
||||
region: content
|
||||
field_notes:
|
||||
type: basic_string
|
||||
label: above
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 6
|
||||
weight: 9
|
||||
region: content
|
||||
field_numero_facture:
|
||||
type: string
|
||||
@@ -102,7 +103,18 @@ content:
|
||||
view_mode: default
|
||||
link: ''
|
||||
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
|
||||
field_type_ligne:
|
||||
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;
|
||||
}
|
||||
|
||||
/* Row layout. Everything not listed here (Montant HT / N° Facture /
|
||||
Montant TTC) is left to grid auto-placement, which -- given they're
|
||||
the only three single-column items appearing consecutively in DOM
|
||||
order -- lines them up as one row of three for free. */
|
||||
/* Row layout, fully explicit (not left to grid auto-placement) --
|
||||
there are now two separate 1-col-plus-2-col row groupings (Client +
|
||||
N° Facture, then Montant HT + TVA + Montant TTC) and relying on DOM
|
||||
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-field-client,
|
||||
.figli-ledger-form > .field--name-field-entree-liee,
|
||||
.figli-ledger-form > .field--name-field-repartition,
|
||||
.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 {
|
||||
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 */
|
||||
.figli-ledger-form .form-item__label {
|
||||
@@ -118,6 +135,14 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
outline: 2px solid var(--flform-accent);
|
||||
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 {
|
||||
width: auto;
|
||||
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_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_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_client', ['type' => 'entity_reference_autocomplete', 'weight' => 2])
|
||||
->setComponent('field_numero_facture', ['type' => 'string_textfield', 'weight' => 3])
|
||||
->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 4])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 5])
|
||||
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 6, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
|
||||
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 7])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 8])
|
||||
->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 5])
|
||||
->setComponent('field_tva', ['type' => 'number', 'weight' => 6])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number', '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_notes', ['type' => 'string_textarea', 'weight' => 9])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 10])
|
||||
->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_client', ['type' => 'entity_reference_label', 'weight' => 2])
|
||||
->setComponent('field_numero_facture', ['type' => 'string', 'weight' => 3])
|
||||
->setComponent('field_montant_ht', ['type' => 'number_decimal', 'weight' => 4])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 5])
|
||||
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 6])
|
||||
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 7])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 8])
|
||||
->setComponent('field_montant_ht', ['type' => 'number_decimal', 'weight' => 5])
|
||||
->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 6])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 7])
|
||||
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 8])
|
||||
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 9])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 10])
|
||||
->save();
|
||||
}
|
||||
}
|
||||
@@ -353,3 +362,122 @@ function figli_compta_ledger_update_8003() {
|
||||
$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
|
||||
|
||||
ledger_form:
|
||||
js:
|
||||
js/ledger-form.js: {}
|
||||
css:
|
||||
theme:
|
||||
css/ledger-form.css: {}
|
||||
dependencies:
|
||||
- core/drupal
|
||||
- core/once
|
||||
|
||||
admin_chrome:
|
||||
css:
|
||||
|
||||
@@ -106,6 +106,17 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
||||
$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';
|
||||
|
||||
$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 --
|
||||
* those get surfaced as visible inconsistencies in the dashboard instead of
|
||||
* 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
|
||||
* associates through the form are never exempted.
|
||||
* flag around such a bulk import to bypass this check *and* the Montant TTC
|
||||
* auto-computation below; new lines entered by associates through the form
|
||||
* are never exempted from either.
|
||||
*/
|
||||
function figli_compta_ledger_node_presave(NodeInterface $node) {
|
||||
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.
|
||||
$node->setRevisionCreationTime(\Drupal::time()->getRequestTime());
|
||||
|
||||
if (!$node->hasField('field_montant_ht') || !$node->hasField('field_repartition')) {
|
||||
if (!$node->hasField('field_montant_ht')) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
$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;
|
||||
foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) {
|
||||
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);
|
||||
Reference in New Issue
Block a user