Mandatory revisions on ligne_comptable + cross-node history page
Every save now forces a new revision, unconditionally: - Form: hide the "Create new revision" checkbox and the log message field (#access = FALSE, not just a default) so submitted values for either can't override them -- Form API discards user input for #access-denied elements and falls back to #default_value. - hook_node_presave(): the same thing enforced for any save that doesn't go through the form (drush scripts, etc.), plus explicitly setting the revision author (current user) and revision timestamp. setNewRevision(TRUE) alone does NOT refresh revision_timestamp -- it carries over the previous revision's value, which would silently mislabel every edit with its predecessor's save time. Verified via a drush test save before/after. - Applies regardless of figli_compta_ledger.skip_validation: that flag is about the répartition-sum check on historical imports, a different concern -- revision history is never exempted. New /lignes/historique page (HistoryController): a single reverse- chronological feed across every ligne_comptable's revisions, gated by the 'view ligne_comptable revisions' permission (granted to all three associate roles). Conceptually a revision of one ligne is a revision of the grand livre as a whole, so this aggregates across nodes rather than reusing Drupal's per-node revision history page. Each row links to that specific revision via core's existing revision-view route. Linked from the /lignes toolbar, shown only when the current user has the permission. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,4 @@ permissions:
|
|||||||
- 'create ligne_comptable content'
|
- 'create ligne_comptable content'
|
||||||
- 'delete any ligne_comptable content'
|
- 'delete any ligne_comptable content'
|
||||||
- 'edit any ligne_comptable content'
|
- 'edit any ligne_comptable content'
|
||||||
|
- 'view ligne_comptable revisions'
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ permissions:
|
|||||||
- 'create ligne_comptable content'
|
- 'create ligne_comptable content'
|
||||||
- 'delete any ligne_comptable content'
|
- 'delete any ligne_comptable content'
|
||||||
- 'edit any ligne_comptable content'
|
- 'edit any ligne_comptable content'
|
||||||
|
- 'view ligne_comptable revisions'
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ uuid: 24f4e46d-c714-486d-b000-f092eb727470
|
|||||||
langcode: en
|
langcode: en
|
||||||
status: true
|
status: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
config:
|
||||||
|
- node.type.ligne_comptable
|
||||||
module:
|
module:
|
||||||
|
- node
|
||||||
- system
|
- system
|
||||||
id: user
|
id: user
|
||||||
label: 'Utilisateur (lecture seule)'
|
label: 'Utilisateur (lecture seule)'
|
||||||
@@ -10,3 +13,4 @@ weight: 4
|
|||||||
is_admin: false
|
is_admin: false
|
||||||
permissions:
|
permissions:
|
||||||
- 'access content'
|
- 'access content'
|
||||||
|
- 'view ligne_comptable revisions'
|
||||||
|
|||||||
@@ -13,3 +13,11 @@ figli_compta_ledger.dashboard:
|
|||||||
menu_name: admin
|
menu_name: admin
|
||||||
parent: system.admin
|
parent: system.admin
|
||||||
weight: -10
|
weight: -10
|
||||||
|
|
||||||
|
figli_compta_ledger.history:
|
||||||
|
title: 'Historique du grand livre'
|
||||||
|
description: 'Toutes les révisions de toutes les lignes comptables'
|
||||||
|
route_name: figli_compta_ledger.history
|
||||||
|
menu_name: admin
|
||||||
|
parent: system.admin
|
||||||
|
weight: -9
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @file
|
* @file
|
||||||
* Enforces the one invariant that explained most of the errors found in the
|
* Enforces two invariants for ligne_comptable: sum(répartition.montant)
|
||||||
* historical spreadsheets: sum(répartition.montant) must equal montant_ht.
|
* must equal montant_ht (explained most of the errors found in the
|
||||||
|
* historical spreadsheets), and every save must create a revision with no
|
||||||
|
* way to opt out, for an audit trail of who changed the shared ledger and
|
||||||
|
* when.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use Drupal\node\NodeInterface;
|
use Drupal\node\NodeInterface;
|
||||||
@@ -33,6 +36,21 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
|||||||
}
|
}
|
||||||
$form['#validate'][] = 'figli_compta_ledger_validate_repartition';
|
$form['#validate'][] = 'figli_compta_ledger_validate_repartition';
|
||||||
|
|
||||||
|
// Every save must create a revision, unconditionally -- there's no
|
||||||
|
// legitimate reason to skip it for an accounting record, so don't let
|
||||||
|
// anyone opt out. #access = FALSE (rather than just leaving the
|
||||||
|
// #default_value alone) makes Form API discard whatever a tampered
|
||||||
|
// request might submit for this field and fall back to #default_value.
|
||||||
|
// figli_compta_ledger_node_presave() enforces the same thing for any
|
||||||
|
// save that doesn't go through this form at all.
|
||||||
|
if (isset($form['revision'])) {
|
||||||
|
$form['revision']['#default_value'] = TRUE;
|
||||||
|
$form['revision']['#access'] = FALSE;
|
||||||
|
}
|
||||||
|
if (isset($form['revision_log'])) {
|
||||||
|
$form['revision_log']['#access'] = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
$request = \Drupal::request();
|
$request = \Drupal::request();
|
||||||
$wrapper_formats = ['drupal_ajax', 'drupal_modal', 'drupal_dialog'];
|
$wrapper_formats = ['drupal_ajax', 'drupal_modal', 'drupal_dialog'];
|
||||||
$is_ajax_modal = in_array($request->query->get('_wrapper_format'), $wrapper_formats, TRUE)
|
$is_ajax_modal = in_array($request->query->get('_wrapper_format'), $wrapper_formats, TRUE)
|
||||||
@@ -110,6 +128,20 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
|
|||||||
if ($node->bundle() !== 'ligne_comptable') {
|
if ($node->bundle() !== 'ligne_comptable') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force a revision on every save, with no log message and the actual
|
||||||
|
// 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());
|
||||||
|
|
||||||
if (!$node->hasField('field_montant_ht') || !$node->hasField('field_repartition')) {
|
if (!$node->hasField('field_montant_ht') || !$node->hasField('field_repartition')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,7 +174,7 @@ function figli_compta_ledger_node_presave(NodeInterface $node) {
|
|||||||
function figli_compta_ledger_theme($existing, $type, $theme, $path) {
|
function figli_compta_ledger_theme($existing, $type, $theme, $path) {
|
||||||
return [
|
return [
|
||||||
'figli_compta_home' => [
|
'figli_compta_home' => [
|
||||||
'variables' => [],
|
'variables' => ['can_view_history' => FALSE],
|
||||||
'template' => 'figli-compta-home',
|
'template' => 'figli-compta-home',
|
||||||
],
|
],
|
||||||
'figli_compta_dashboard' => [
|
'figli_compta_dashboard' => [
|
||||||
|
|||||||
@@ -13,3 +13,11 @@ figli_compta_ledger.dashboard:
|
|||||||
_title: 'Tableau de bord - SAS Figures Libres'
|
_title: 'Tableau de bord - SAS Figures Libres'
|
||||||
requirements:
|
requirements:
|
||||||
_permission: 'access content'
|
_permission: 'access content'
|
||||||
|
|
||||||
|
figli_compta_ledger.history:
|
||||||
|
path: '/lignes/historique'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\figli_compta_ledger\Controller\HistoryController::history'
|
||||||
|
_title: 'Historique du grand livre - SAS Figures Libres'
|
||||||
|
requirements:
|
||||||
|
_permission: 'view ligne_comptable revisions'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DashboardController extends ControllerBase {
|
|||||||
public function home() {
|
public function home() {
|
||||||
return [
|
return [
|
||||||
'#theme' => 'figli_compta_home',
|
'#theme' => 'figli_compta_home',
|
||||||
|
'#can_view_history' => $this->currentUser()->hasPermission('view ligne_comptable revisions'),
|
||||||
'#attached' => [
|
'#attached' => [
|
||||||
'library' => ['figli_compta_ledger/home'],
|
'library' => ['figli_compta_ledger/home'],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\figli_compta_ledger\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grand livre history: a single reverse-chronological feed across every
|
||||||
|
* ligne_comptable's revisions, rather than Drupal's default per-node
|
||||||
|
* history page -- a revision of one ligne is, conceptually, a revision of
|
||||||
|
* the grand livre as a whole.
|
||||||
|
*/
|
||||||
|
class HistoryController extends ControllerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists every ligne_comptable revision, most recent change first.
|
||||||
|
*/
|
||||||
|
public function history() {
|
||||||
|
$node_storage = $this->entityTypeManager()->getStorage('node');
|
||||||
|
$date_formatter = \Drupal::service('date.formatter');
|
||||||
|
|
||||||
|
$revision_ids = $this->entityTypeManager()->getStorage('node')->getQuery()
|
||||||
|
->accessCheck(TRUE)
|
||||||
|
->allRevisions()
|
||||||
|
->condition('type', 'ligne_comptable')
|
||||||
|
->sort('revision_timestamp', 'DESC')
|
||||||
|
->pager(50)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($revision_ids as $vid => $nid) {
|
||||||
|
$revision = $node_storage->loadRevision($vid);
|
||||||
|
if (!$revision) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$author = $revision->getRevisionUser();
|
||||||
|
$rows[] = [
|
||||||
|
$date_formatter->format($revision->getRevisionCreationTime(), 'short'),
|
||||||
|
$author ? $author->getDisplayName() : $this->t('Utilisateur supprimé'),
|
||||||
|
$revision->hasField('field_date_ligne') ? $revision->get('field_date_ligne')->value : '',
|
||||||
|
$revision->label(),
|
||||||
|
$revision->hasField('field_montant_ht') && !$revision->get('field_montant_ht')->isEmpty()
|
||||||
|
? $revision->get('field_montant_ht')->value . ' €'
|
||||||
|
: '',
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'#type' => 'link',
|
||||||
|
'#title' => $this->t('Voir cette version'),
|
||||||
|
'#url' => Url::fromRoute('entity.node.revision', [
|
||||||
|
'node' => $nid,
|
||||||
|
'node_revision' => $vid,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'table' => [
|
||||||
|
'#type' => 'table',
|
||||||
|
'#header' => [
|
||||||
|
$this->t('Modifié le'),
|
||||||
|
$this->t('Par'),
|
||||||
|
$this->t('Date de la ligne'),
|
||||||
|
$this->t('Ligne'),
|
||||||
|
$this->t('Montant HT'),
|
||||||
|
$this->t('Version'),
|
||||||
|
],
|
||||||
|
'#rows' => $rows,
|
||||||
|
'#empty' => $this->t('Aucune révision.'),
|
||||||
|
'#attributes' => ['class' => ['figli-historique-table']],
|
||||||
|
],
|
||||||
|
'pager' => [
|
||||||
|
'#type' => 'pager',
|
||||||
|
],
|
||||||
|
'#cache' => [
|
||||||
|
'contexts' => ['user.permissions'],
|
||||||
|
'tags' => ['node_list:ligne_comptable'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
<div id="figli-home-app">
|
<div id="figli-home-app">
|
||||||
<div class="figli-toolbar">
|
<div class="figli-toolbar">
|
||||||
<a href="/node/add/ligne_comptable" class="button button--primary" @click.prevent="openAddForm">+ Ajouter une ligne</a>
|
<a href="/node/add/ligne_comptable" class="button button--primary" @click.prevent="openAddForm">+ Ajouter une ligne</a>
|
||||||
|
{% endverbatim %}
|
||||||
|
{% if can_view_history %}
|
||||||
|
<a href="{{ path('figli_compta_ledger.history') }}" class="button">Historique</a>
|
||||||
|
{% endif %}
|
||||||
|
{% verbatim %}
|
||||||
|
|
||||||
<label>Compte
|
<label>Compte
|
||||||
<select v-model="filterCompte">
|
<select v-model="filterCompte">
|
||||||
|
|||||||
Reference in New Issue
Block a user