diff --git a/config/sync/core.entity_form_display.node.ligne_comptable.default.yml b/config/sync/core.entity_form_display.node.ligne_comptable.default.yml index 0bacc0e..0a31aa3 100644 --- a/config/sync/core.entity_form_display.node.ligne_comptable.default.yml +++ b/config/sync/core.entity_form_display.node.ligne_comptable.default.yml @@ -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 diff --git a/config/sync/core.entity_view_display.node.ligne_comptable.default.yml b/config/sync/core.entity_view_display.node.ligne_comptable.default.yml index a015afe..2d5cfb8 100644 --- a/config/sync/core.entity_view_display.node.ligne_comptable.default.yml +++ b/config/sync/core.entity_view_display.node.ligne_comptable.default.yml @@ -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 diff --git a/config/sync/field.field.node.ligne_comptable.field_tva.yml b/config/sync/field.field.node.ligne_comptable.field_tva.yml new file mode 100644 index 0000000..2501fe3 --- /dev/null +++ b/config/sync/field.field.node.ligne_comptable.field_tva.yml @@ -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 diff --git a/config/sync/field.storage.node.field_tva.yml b/config/sync/field.storage.node.field_tva.yml new file mode 100644 index 0000000..87c88ca --- /dev/null +++ b/config/sync/field.storage.node.field_tva.yml @@ -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 diff --git a/web/modules/custom/figli_compta_ledger/css/ledger-form.css b/web/modules/custom/figli_compta_ledger/css/ledger-form.css index 5efddd6..587bc28 100644 --- a/web/modules/custom/figli_compta_ledger/css/ledger-form.css +++ b/web/modules/custom/figli_compta_ledger/css/ledger-form.css @@ -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; diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.install b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.install index a43b1a2..85280c1 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.install +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.install @@ -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)."; +} diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml index b66eab1..e26db2f 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml @@ -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: diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module index ff6dbae..f5cc7ea 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module @@ -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()) { diff --git a/web/modules/custom/figli_compta_ledger/js/ledger-form.js b/web/modules/custom/figli_compta_ledger/js/ledger-form.js new file mode 100644 index 0000000..ccb8906 --- /dev/null +++ b/web/modules/custom/figli_compta_ledger/js/ledger-form.js @@ -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