Ajoute field_ecart (Montant HT - somme répartition), stocké à la sauvegarde
Phase 1 du chantier filtrage serveur de /lignes : l'écart devient une vraie valeur stockée plutôt que recalculée à la volée en résolvant les paragraphes de répartition, pour que le futur filtre "Écarts uniquement" côté serveur puisse filtrer directement dessus. Contrairement au Montant TTC (protégé par un garde-fou pour ne jamais altérer un TTC historique), l'écart n'a pas de valeur passée à protéger -- il reflète l'état *actuel* de la répartition, donc recalculé sans condition à chaque sauvegarde. figli_compta_ledger_ node_presave() calculait déjà cette somme pour la validation répartition == HT ; le stockage était quasi gratuit à ajouter au même endroit. Migration (update hooks 8009/8010) : rétro-calcul mécanique pour les 1556 lignes existantes (tous types, contrairement aux migrations TVA/ cotisation qui ne concernaient que les entrées client) -- aucune ambiguïté à arbitrer, juste HT moins répartition. Vérifié indépendamment ligne par ligne après coup : 0 écart entre la valeur recalculée à la main et celle stockée par la migration. Nouveau flag d'état 'figli_compta_ledger.skip_revision', utilisé uniquement par cette migration : backfiller un champ purement calculé sur 1556 lignes déjà migrées n'est pas un changement éditorial qui justifie 1556 nouvelles révisions -- vérifié qu'aucune révision supplémentaire n'a été créée. Reste indépendant de skip_validation (déjà utilisé par tous les autres scripts de migration de ce module, et qui doit continuer à créer une révision). Restructuration de figli_compta_ledger_node_presave() : le calcul de l'écart et son stockage se font maintenant même sous skip_validation (seul le lancement de l'exception reste conditionnel) -- vérifié que l'exception se déclenche toujours normalement sur une vraie répartition incohérente, et que Montant TTC/Cotisation restent inchangés sur une sauvegarde qui ne touche ni HT ni TVA.
This commit is contained in:
@@ -226,6 +226,11 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
|
||||
_figli_field('node', 'ligne_comptable', 'field_cotisation_active', 'Cotisation diffuseur URSSAF (1,1%)', 'boolean');
|
||||
_figli_field('node', 'ligne_comptable', 'field_cotisation_urssaf', '1,1%', 'decimal', ['precision' => 12, 'scale' => 2]);
|
||||
_figli_field('node', 'ligne_comptable', 'field_montant_ttc', 'Montant TTC (€)', 'decimal', ['precision' => 12, 'scale' => 2]);
|
||||
// Montant HT - somme(répartition), kept in sync at every save (see
|
||||
// figli_compta_ledger_node_presave()) so /lignes' "Écarts uniquement"
|
||||
// filter and its future server-side equivalent can filter on a real
|
||||
// stored value instead of resolving répartition paragraphs per row.
|
||||
_figli_field('node', 'ligne_comptable', 'field_ecart', 'Écart', 'decimal', ['precision' => 12, 'scale' => 2]);
|
||||
_figli_field('node', 'ligne_comptable', 'field_notes', 'Notes / détail', 'string_long');
|
||||
|
||||
// Free-tagging signalement (e.g. "client impayé", "à relancer") -- purely
|
||||
@@ -723,3 +728,82 @@ function figli_compta_ledger_update_8008() {
|
||||
|
||||
return "field_cotisation_urssaf corrigé (delta seul, pas HT + delta) pour $fixed lignes.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds field_ecart (Montant HT - somme répartition) to ligne_comptable --
|
||||
* kept in sync at every save from now on (see
|
||||
* figli_compta_ledger_node_presave()), so the server-side filtering work
|
||||
* planned for /lignes can filter on a real stored value instead of
|
||||
* resolving répartition paragraphs per row on every request. Existing
|
||||
* content backfilled separately, see figli_compta_ledger_update_8010().
|
||||
*/
|
||||
function figli_compta_ledger_update_8009() {
|
||||
// No form widget added -- purely a diagnostic/filtering value, already
|
||||
// fully represented in the edit form by Montant HT + the Répartition
|
||||
// paragraphs it's derived from, and already shown on /lignes' own
|
||||
// Écart column. Nothing new for an associate to look at here; simply
|
||||
// never calling setComponent() for it on the form display is enough
|
||||
// to keep it off that form.
|
||||
_figli_field('node', 'ligne_comptable', 'field_ecart', 'Écart', 'decimal', ['precision' => 12, 'scale' => 2]);
|
||||
|
||||
$view_display = EntityViewDisplay::load('node.ligne_comptable.default');
|
||||
if ($view_display && !$view_display->getComponent('field_ecart')) {
|
||||
$view_display->setComponent('field_ecart', ['type' => 'number_decimal', 'weight' => 13])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfills field_ecart for every existing ligne_comptable (all types,
|
||||
* unlike the TVA/cotisation migrations which only ever touched "Entrée
|
||||
* client" lines -- répartition applies to every line regardless of
|
||||
* type). Mechanical, not a judgment call like
|
||||
* figli_compta_ledger_update_8007()'s TVA correction: Montant HT and the
|
||||
* répartition amounts are both already-correct historical values, this
|
||||
* just computes their difference and stores it, exactly what
|
||||
* figli_compta_ledger_node_presave() will do going forward.
|
||||
*
|
||||
* Runs under two state flags, not just the usual skip_validation:
|
||||
* skip_validation avoids tripping the répartition-sum exception on the
|
||||
* ~18 lines with a known, deliberately-preserved historical mismatch
|
||||
* (same as every other bulk script in this module); skip_revision
|
||||
* additionally suppresses the forced-revision block in
|
||||
* figli_compta_ledger_node_presave() -- backfilling a purely computed
|
||||
* diagnostic field onto ~1500 already-migrated lines isn't an editorial
|
||||
* change worth 1500 new revisions cluttering the audit trail that
|
||||
* mechanism exists to protect. Montant TTC is never touched either way
|
||||
* (skip_validation already keeps it out of reach, see
|
||||
* figli_compta_ledger_node_presave()).
|
||||
*/
|
||||
function figli_compta_ledger_update_8010() {
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$nids = $storage->getQuery()
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'ligne_comptable')
|
||||
->execute();
|
||||
|
||||
$filled = 0;
|
||||
$skipped = 0;
|
||||
\Drupal::state()->set('figli_compta_ledger.skip_validation', TRUE);
|
||||
\Drupal::state()->set('figli_compta_ledger.skip_revision', TRUE);
|
||||
foreach ($storage->loadMultiple($nids) as $node) {
|
||||
if (!$node->hasField('field_montant_ht') || $node->get('field_montant_ht')->isEmpty()
|
||||
|| !$node->hasField('field_repartition')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$ht = (float) $node->get('field_montant_ht')->value;
|
||||
$somme = 0.0;
|
||||
foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) {
|
||||
if ($paragraph->hasField('field_montant') && !$paragraph->get('field_montant')->isEmpty()) {
|
||||
$somme += (float) $paragraph->get('field_montant')->value;
|
||||
}
|
||||
}
|
||||
$node->set('field_ecart', round($ht - $somme, 2));
|
||||
$node->save();
|
||||
$filled++;
|
||||
}
|
||||
\Drupal::state()->delete('figli_compta_ledger.skip_validation');
|
||||
\Drupal::state()->delete('figli_compta_ledger.skip_revision');
|
||||
|
||||
return "Écart calculé pour $filled lignes (aucune nouvelle révision créée), $skipped laissées vides (pas de Montant HT).";
|
||||
}
|
||||
|
||||
@@ -330,7 +330,11 @@ function figli_compta_ledger_node_form_ajax_submit(array $form, FormStateInterfa
|
||||
* being silently fixed. Set the 'figli_compta_ledger.skip_validation' state
|
||||
* 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.
|
||||
* are never exempted from either. field_ecart is the one thing still kept
|
||||
* in sync even under skip_validation (see below) -- separately, a second
|
||||
* flag ('figli_compta_ledger.skip_revision') additionally suppresses the
|
||||
* forced-revision block, used only by figli_compta_ledger_update_8010()'s
|
||||
* field_ecart backfill.
|
||||
*/
|
||||
function figli_compta_ledger_node_presave(NodeInterface $node) {
|
||||
if ($node->bundle() !== 'ligne_comptable') {
|
||||
@@ -341,81 +345,89 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
|
||||
// acting user as its author -- this is the audit trail for a shared
|
||||
// ledger, not the répartition-sum check below, so it's never exempted
|
||||
// (not even during a skip_validation import: migrated data still needs
|
||||
// an honest revision history).
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->setRevisionLogMessage('');
|
||||
$node->setRevisionUserId(\Drupal::currentUser()->id());
|
||||
// setNewRevision() alone doesn't refresh this -- it stays whatever it
|
||||
// was on the entity as loaded (the previous revision's timestamp),
|
||||
// silently mislabeling every edit with its predecessor's save time.
|
||||
$node->setRevisionCreationTime(\Drupal::time()->getRequestTime());
|
||||
// an honest revision history). The one deliberate exception:
|
||||
// figli_compta_ledger_update_8010()'s one-off field_ecart backfill sets
|
||||
// 'figli_compta_ledger.skip_revision' -- a purely computed diagnostic
|
||||
// value being backfilled onto ~1500 already-migrated lines isn't an
|
||||
// editorial change worth 1500 new revisions cluttering the very audit
|
||||
// trail this exists to protect.
|
||||
if (!\Drupal::state()->get('figli_compta_ledger.skip_revision', FALSE)) {
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->setRevisionLogMessage('');
|
||||
$node->setRevisionUserId(\Drupal::currentUser()->id());
|
||||
// setNewRevision() alone doesn't refresh this -- it stays whatever it
|
||||
// was on the entity as loaded (the previous revision's timestamp),
|
||||
// silently mislabeling every edit with its predecessor's save time.
|
||||
$node->setRevisionCreationTime(\Drupal::time()->getRequestTime());
|
||||
}
|
||||
|
||||
if (!$node->hasField('field_montant_ht')) {
|
||||
return;
|
||||
}
|
||||
if (\Drupal::state()->get('figli_compta_ledger.skip_validation', FALSE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$skip_validation = \Drupal::state()->get('figli_compta_ledger.skip_validation', FALSE);
|
||||
$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;
|
||||
$type = $node->hasField('field_type_ligne') ? $node->get('field_type_ligne')->value : NULL;
|
||||
$cotisation_active = $node->hasField('field_cotisation_active') && !$node->get('field_cotisation_active')->isEmpty()
|
||||
? (bool) $node->get('field_cotisation_active')->value : FALSE;
|
||||
// Only "Entrée client" lines ever go through a client devis -- the
|
||||
// only place the cotisation diffuseur is invoiced at all (see
|
||||
// figli_compta_ledger_update_8006()'s docblock) -- so the checkbox is
|
||||
// ignored for every other type even if it somehow ended up checked
|
||||
// (e.g. a line's type changed after the fact).
|
||||
$cotisation_applies = $type === 'entree' && $cotisation_active;
|
||||
|
||||
// Montant TTC is no longer entered by hand -- it's a pure function of
|
||||
// Montant HT, TVA, and (entrée client only) the cotisation diffuseur
|
||||
// URSSAF cascade (see figli_compta_ledger_update_8004()/_8006()). Only
|
||||
// recompute it when HT, TVA, type, or the cotisation toggle actually
|
||||
// changed, though, rather than on every save unconditionally: the TVA
|
||||
// backfilled onto pre-migration lines (figli_compta_ledger_update_8005()/
|
||||
// _8007()) 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, TVA, type, or cotisation toggle has no such history
|
||||
// to protect.
|
||||
if ($node->hasField('field_montant_ttc')) {
|
||||
$inputs_changed = $node->isNew();
|
||||
if (!$inputs_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;
|
||||
$orig_type = $original->hasField('field_type_ligne') ? $original->get('field_type_ligne')->value : NULL;
|
||||
$orig_cotisation_active = $original->hasField('field_cotisation_active') && !$original->get('field_cotisation_active')->isEmpty()
|
||||
? (bool) $original->get('field_cotisation_active')->value : FALSE;
|
||||
$inputs_changed = $orig_ht !== $montant_ht || abs($orig_tva - $tva) > 0.00005
|
||||
|| $orig_type !== $type || $orig_cotisation_active !== $cotisation_active;
|
||||
}
|
||||
if ($inputs_changed) {
|
||||
if ($cotisation_applies) {
|
||||
// field_cotisation_urssaf stores just the 1,1% itself (the
|
||||
// delta), not Montant HT + 1,1% -- that's what "1,1%" as a
|
||||
// column/field label means. TVA still applies to the augmented
|
||||
// base though, so $base (used for Montant TTC just below) stays
|
||||
// HT + delta; only what gets *written* to the field changes.
|
||||
$delta = round($montant_ht * 0.011, 2);
|
||||
$base = $montant_ht + $delta;
|
||||
if ($node->hasField('field_cotisation_urssaf')) {
|
||||
$node->set('field_cotisation_urssaf', $delta);
|
||||
}
|
||||
if (!$skip_validation) {
|
||||
$tva = $node->hasField('field_tva') && !$node->get('field_tva')->isEmpty() ? (float) $node->get('field_tva')->value : 0.0;
|
||||
$type = $node->hasField('field_type_ligne') ? $node->get('field_type_ligne')->value : NULL;
|
||||
$cotisation_active = $node->hasField('field_cotisation_active') && !$node->get('field_cotisation_active')->isEmpty()
|
||||
? (bool) $node->get('field_cotisation_active')->value : FALSE;
|
||||
// Only "Entrée client" lines ever go through a client devis -- the
|
||||
// only place the cotisation diffuseur is invoiced at all (see
|
||||
// figli_compta_ledger_update_8006()'s docblock) -- so the checkbox is
|
||||
// ignored for every other type even if it somehow ended up checked
|
||||
// (e.g. a line's type changed after the fact).
|
||||
$cotisation_applies = $type === 'entree' && $cotisation_active;
|
||||
|
||||
// Montant TTC is no longer entered by hand -- it's a pure function of
|
||||
// Montant HT, TVA, and (entrée client only) the cotisation diffuseur
|
||||
// URSSAF cascade (see figli_compta_ledger_update_8004()/_8006()). Only
|
||||
// recompute it when HT, TVA, type, or the cotisation toggle actually
|
||||
// changed, though, rather than on every save unconditionally: the TVA
|
||||
// backfilled onto pre-migration lines (figli_compta_ledger_update_8005()/
|
||||
// _8007()) 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, TVA, type, or cotisation toggle has no such history
|
||||
// to protect.
|
||||
if ($node->hasField('field_montant_ttc')) {
|
||||
$inputs_changed = $node->isNew();
|
||||
if (!$inputs_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;
|
||||
$orig_type = $original->hasField('field_type_ligne') ? $original->get('field_type_ligne')->value : NULL;
|
||||
$orig_cotisation_active = $original->hasField('field_cotisation_active') && !$original->get('field_cotisation_active')->isEmpty()
|
||||
? (bool) $original->get('field_cotisation_active')->value : FALSE;
|
||||
$inputs_changed = $orig_ht !== $montant_ht || abs($orig_tva - $tva) > 0.00005
|
||||
|| $orig_type !== $type || $orig_cotisation_active !== $cotisation_active;
|
||||
}
|
||||
else {
|
||||
$base = $montant_ht;
|
||||
if ($node->hasField('field_cotisation_urssaf')) {
|
||||
$node->set('field_cotisation_urssaf', NULL);
|
||||
if ($inputs_changed) {
|
||||
if ($cotisation_applies) {
|
||||
// field_cotisation_urssaf stores just the 1,1% itself (the
|
||||
// delta), not Montant HT + 1,1% -- that's what "1,1%" as a
|
||||
// column/field label means. TVA still applies to the augmented
|
||||
// base though, so $base (used for Montant TTC just below) stays
|
||||
// HT + delta; only what gets *written* to the field changes.
|
||||
$delta = round($montant_ht * 0.011, 2);
|
||||
$base = $montant_ht + $delta;
|
||||
if ($node->hasField('field_cotisation_urssaf')) {
|
||||
$node->set('field_cotisation_urssaf', $delta);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$base = $montant_ht;
|
||||
if ($node->hasField('field_cotisation_urssaf')) {
|
||||
$node->set('field_cotisation_urssaf', NULL);
|
||||
}
|
||||
}
|
||||
$node->set('field_montant_ttc', round($base * (1 + $tva / 100), 2));
|
||||
}
|
||||
$node->set('field_montant_ttc', round($base * (1 + $tva / 100), 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +443,21 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
|
||||
}
|
||||
|
||||
$ecart = round($montant_ht - $somme, 2);
|
||||
|
||||
// field_ecart mirrors this sum -- kept in sync unconditionally,
|
||||
// regardless of skip_validation (see figli_compta_ledger_update_8009()'s
|
||||
// docblock): unlike Montant TTC, there's no historical value to protect
|
||||
// here, the field's whole purpose is to reflect the *current*
|
||||
// répartition state, not a preserved snapshot -- and it's what /lignes'
|
||||
// "Écarts uniquement" filter will eventually filter on server-side.
|
||||
if ($node->hasField('field_ecart')) {
|
||||
$node->set('field_ecart', $ecart);
|
||||
}
|
||||
|
||||
if ($skip_validation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (abs($ecart) > 0.01) {
|
||||
throw new EntityStorageException(sprintf(
|
||||
"Répartition incohérente : la somme des comptes (%.2f €) ne correspond pas au montant HT (%.2f €). Écart : %.2f €. Corrigez la répartition avant d'enregistrer.",
|
||||
|
||||
Reference in New Issue
Block a user