Compare commits
5
Commits
9234050a68
...
f0927bac88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0927bac88 | ||
|
|
5aa00b2ffd | ||
|
|
b7490eb76a | ||
|
|
f00f680143 | ||
|
|
4ce1b12099 |
@@ -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: 11
|
||||||
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: 10
|
||||||
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: 9
|
||||||
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: 8
|
||||||
|
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: 11
|
||||||
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: 10
|
||||||
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: 9
|
||||||
|
region: content
|
||||||
|
field_tva:
|
||||||
|
type: number_decimal
|
||||||
|
label: above
|
||||||
|
settings:
|
||||||
|
thousand_separator: ''
|
||||||
|
decimal_separator: .
|
||||||
|
scale: 2
|
||||||
|
prefix_suffix: true
|
||||||
|
third_party_settings: { }
|
||||||
|
weight: 8
|
||||||
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: 10.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
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
/* Ligne comptable add/edit form (node/add/ligne_comptable, node/X/edit --
|
||||||
|
usually seen inside the "+ Ajouter une ligne" / "Modifier" AJAX modal
|
||||||
|
from /lignes, see js/home.js's openAddForm/openEditForm). The default
|
||||||
|
Drupal/Gin admin form widgets (tabledrag drag handles, the paragraphs
|
||||||
|
"Toggle Actions" dropdown, vertical tabs rendered as an accordion) are
|
||||||
|
built for content editors managing arbitrary nodes, not for the small
|
||||||
|
numeric entry form associates fill in daily. This only restyles it
|
||||||
|
into a compact grid -- no Form API behavior, validation, or field
|
||||||
|
structure changes here (those live in figli_compta_ledger.module,
|
||||||
|
which also hides the "advanced" accordion and "Published" checkbox
|
||||||
|
entirely rather than just visually, since neither is ever used for a
|
||||||
|
ligne_comptable).
|
||||||
|
|
||||||
|
Explicit local colors rather than Gin's --gin-* custom properties --
|
||||||
|
this form isn't scoped inside #figli-home-app/#figli-dashboard-app, so
|
||||||
|
the same "don't trust Gin's vars on custom routes" lesson from
|
||||||
|
home.css/dashboard.css applies here too. */
|
||||||
|
.figli-ledger-form {
|
||||||
|
--flform-border: #dcdee2;
|
||||||
|
--flform-border-soft: #e8eaed;
|
||||||
|
--flform-label: #4b5563;
|
||||||
|
--flform-text: #1a1a1a;
|
||||||
|
--flform-bg-subtle: #f7f8fa;
|
||||||
|
--flform-accent: #2f6f4f;
|
||||||
|
--flform-danger: #b3261e;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
max-width: 44rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.15rem 0.1rem 0.5rem;
|
||||||
|
color: var(--flform-text);
|
||||||
|
}
|
||||||
|
/* Claro gives every .form-item a large margin-block (1.5rem, meant for a
|
||||||
|
form with one field per row) -- with our own grid `gap` now doing that
|
||||||
|
job, this just doubles up the spacing between rows. */
|
||||||
|
.figli-ledger-form .form-item {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
html.gin--dark-mode .figli-ledger-form {
|
||||||
|
--flform-border: #3d3e42;
|
||||||
|
--flform-border-soft: #333438;
|
||||||
|
--flform-label: #a1a5ab;
|
||||||
|
--flform-text: #e8e9ea;
|
||||||
|
--flform-bg-subtle: #2a2b2e;
|
||||||
|
--flform-accent: #5fb98a;
|
||||||
|
--flform-danger: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper divs Drupal still renders even though they're always empty
|
||||||
|
here (single-language site, path aliases handled elsewhere, status
|
||||||
|
hidden via #access in the module) -- without this they'd each still
|
||||||
|
claim an empty grid cell. */
|
||||||
|
.figli-ledger-form > .field--name-langcode,
|
||||||
|
.figli-ledger-form > .field--name-path,
|
||||||
|
.figli-ledger-form > .entity-content-form-footer,
|
||||||
|
.figli-ledger-form > .form-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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-entree-liee,
|
||||||
|
.figli-ledger-form > .field--name-field-repartition,
|
||||||
|
.figli-ledger-form > .field--name-field-notes,
|
||||||
|
.figli-ledger-form > .field--name-field-flag {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.figli-ledger-form > .field--name-field-date-ligne {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
/* The field_tva_rate select (added in figli_compta_ledger_form_alter(),
|
||||||
|
not a real field so it gets no field--name-* class) fills the middle
|
||||||
|
slot of the HT/TVA/TTC row instead -- see that function's #weight
|
||||||
|
comment for why this specific ordering (weight 6, between HT's 5 and
|
||||||
|
TTC's 7) matters, not just matching column numbers. field_tva itself
|
||||||
|
only appears -- full width, its own row -- when "Autre" is picked. */
|
||||||
|
.figli-ledger-form > .js-form-item-field-tva-rate {
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
}
|
||||||
|
.figli-ledger-form > .field--name-field-tva {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.figli-ledger-form > .field--name-field-montant-ttc {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field basics */
|
||||||
|
.figli-ledger-form .form-item__label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--flform-label);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.figli-ledger-form .form-item__description {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--flform-label);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form input.form-element,
|
||||||
|
.figli-ledger-form select.form-element,
|
||||||
|
.figli-ledger-form textarea.form-element {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--flform-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.32rem 0.5rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--flform-text);
|
||||||
|
}
|
||||||
|
/* Not select: Claro positions its dropdown-chevron background-image via
|
||||||
|
background-position/-repeat/-size, which the `background` shorthand
|
||||||
|
below would silently reset to their initial values (top-left,
|
||||||
|
repeating) -- that turned the single right-aligned arrow into a tiled
|
||||||
|
pattern smeared across the whole field. Selects keep Claro's own
|
||||||
|
background untouched; only the border/radius/padding above apply. */
|
||||||
|
.figli-ledger-form input.form-element,
|
||||||
|
.figli-ledger-form textarea.form-element {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.figli-ledger-form input.form-element:focus,
|
||||||
|
.figli-ledger-form select.form-element:focus,
|
||||||
|
.figli-ledger-form textarea.form-element:focus {
|
||||||
|
outline: 2px solid var(--flform-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
/* Montant TTC: #disabled, computed from HT + TVA (see
|
||||||
|
figli_compta_ledger_node_presave()) -- a faint fill and full opacity
|
||||||
|
(overriding the browser's default half-transparent disabled look,
|
||||||
|
which would make the computed number harder to read) instead of the
|
||||||
|
plain white/transparent of an editable field. -webkit-text-fill-color
|
||||||
|
specifically: Chromium renders disabled/autofilled input text through
|
||||||
|
this property instead of `color` -- left alone, it defaults to a
|
||||||
|
fixed grey that's illegible against either theme's --flform-bg-subtle
|
||||||
|
(near-invisible dark-grey-on-dark-grey in dark mode, the actual bug
|
||||||
|
reported; same risk in light mode against a light fill). */
|
||||||
|
.figli-ledger-form input:disabled.form-element {
|
||||||
|
background: var(--flform-bg-subtle);
|
||||||
|
color: var(--flform-text);
|
||||||
|
-webkit-text-fill-color: var(--flform-text);
|
||||||
|
opacity: 1;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.figli-ledger-form input[type="date"].form-element {
|
||||||
|
width: auto;
|
||||||
|
min-width: 9.5rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form textarea.form-element {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multi-value tables (Répartition, Entrée client liée): drop the drag
|
||||||
|
handle and the "Show row weights" accessibility toggle for it -- row
|
||||||
|
order never affects the répartition sum or anything displayed
|
||||||
|
downstream, so with the usual 1-3 rows this is pure clutter. Remove
|
||||||
|
isn't affected: it stays in the per-row "Toggle Actions" menu. */
|
||||||
|
.figli-ledger-form .tabledrag-toggle-weight-wrapper,
|
||||||
|
.figli-ledger-form td.field-multiple-drag {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.figli-ledger-form table.field-multiple-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--flform-bg-subtle);
|
||||||
|
border: 1px solid var(--flform-border-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.figli-ledger-form table.field-multiple-table thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.4rem 0.6rem 0.25rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form table.field-multiple-table tbody td {
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
vertical-align: top;
|
||||||
|
border-top: 1px solid var(--flform-border-soft);
|
||||||
|
}
|
||||||
|
.figli-ledger-form .field-multiple-table .form-item__label {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.figli-ledger-form .paragraphs-subform {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form .paragraphs-subform > .js-form-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons: "Ajouter Répartition" / "Add another item", and the per-row
|
||||||
|
Remove/Duplicate/Collapse actions -- all plain Drupal form-submit
|
||||||
|
buttons by default, restyled as a small, consistent set instead of
|
||||||
|
browser-default grey buttons of varying sizes. */
|
||||||
|
.figli-ledger-form input.button {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--flform-border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--flform-text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.figli-ledger-form input.button:hover {
|
||||||
|
border-color: var(--flform-accent);
|
||||||
|
color: var(--flform-accent);
|
||||||
|
}
|
||||||
|
.figli-ledger-form .field-add-more-submit {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form .paragraphs-dropdown-toggle {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--flform-label);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
}
|
||||||
|
.figli-ledger-form .paragraphs-dropdown-toggle:hover {
|
||||||
|
color: var(--flform-accent);
|
||||||
|
}
|
||||||
|
/* Closed by default (Claro's own paragraphs.actions.css already sets
|
||||||
|
display: none here and toggles a sibling .open class on click via
|
||||||
|
paragraphs.actions.js) -- only restyle the panel's look, and only once
|
||||||
|
Drupal's own JS has actually opened it. Getting this wrong (e.g.
|
||||||
|
forcing `display: flex` unconditionally here) makes every "Toggle
|
||||||
|
Actions" menu render permanently expanded. */
|
||||||
|
.figli-ledger-form .paragraphs-dropdown.open .paragraphs-dropdown-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.3rem;
|
||||||
|
border: 1px solid var(--flform-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--flform-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field group heading style (the Répartition table's own label, inside
|
||||||
|
its <th>) -- match the rest of the form's label typography instead of
|
||||||
|
Drupal's default <h4>. */
|
||||||
|
.figli-ledger-form table.field-multiple-table h4.form-item__label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A répartition row flagged by the sum-mismatch #validate error (see
|
||||||
|
figli_compta_ledger_validate_repartition()) -- kept subtle (a red
|
||||||
|
outline, not a solid fill) to match the same red-liseré convention
|
||||||
|
used for écarts in the /lignes table itself. */
|
||||||
|
.figli-ledger-form tr.error,
|
||||||
|
.figli-ledger-form .has-error {
|
||||||
|
outline: 1px solid var(--flform-danger);
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form actions (Save), rendered by the AJAX dialog's own button pane
|
||||||
|
rather than here in most cases, but keep a sane baseline for the
|
||||||
|
rare full-page (non-modal) view of this form. */
|
||||||
|
.figli-ledger-form .form-actions input.button--primary {
|
||||||
|
background: var(--flform-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--flform-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -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,19 @@ 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_montant_ttc', ['type' => 'number', 'weight' => 7])
|
||||||
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 6, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
|
// field_tva sits *after* Montant TTC, not between HT and TTC --
|
||||||
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 7])
|
// figli_compta_ledger_form_alter() inserts a non-field
|
||||||
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 8])
|
// "field_tva_rate" select at weight 6 (a select of the official
|
||||||
|
// French VAT rates) to fill that visual slot instead; this real
|
||||||
|
// field only becomes visible (still at its own weight, on its own
|
||||||
|
// full-width row) when "Autre" is picked there. See
|
||||||
|
// css/ledger-form.css's grid-column rules for both.
|
||||||
|
->setComponent('field_tva', ['type' => 'number', 'weight' => 8])
|
||||||
|
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 9, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
|
||||||
|
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 10])
|
||||||
|
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 11])
|
||||||
->save();
|
->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,11 +269,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_montant_ttc', ['type' => 'number_decimal', 'weight' => 7])
|
||||||
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 6])
|
->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 8])
|
||||||
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 7])
|
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 9])
|
||||||
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 8])
|
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 10])
|
||||||
|
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 11])
|
||||||
->save();
|
->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,3 +369,127 @@ 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 10% (taux intermédiaire) rather than blank --
|
||||||
|
// most new lines associates enter do carry VAT, so this saves a click
|
||||||
|
// on the common case instead of silently defaulting to "no VAT" for
|
||||||
|
// every line unless someone remembers to change it.
|
||||||
|
$field_tva = FieldConfig::loadByName('node', 'ligne_comptable', 'field_tva');
|
||||||
|
if ($field_tva && $field_tva->getDefaultValueLiteral() === []) {
|
||||||
|
$field_tva->setDefaultValue(10)->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_montant_ttc', ['type' => 'number', 'weight' => 7] + (array) $form_display->getComponent('field_montant_ttc'));
|
||||||
|
// Weight 6 (between HT and TTC) is now the non-field
|
||||||
|
// "field_tva_rate" select figli_compta_ledger_form_alter() adds --
|
||||||
|
// field_tva itself moves after Répartition/Notes/Flag's *old*
|
||||||
|
// positions, past the ones below, so it lands on its own row when
|
||||||
|
// "Autre" reveals it instead of disrupting the HT/rate/TTC row.
|
||||||
|
$form_display->setComponent('field_tva', ['type' => 'number', 'weight' => 8] + (array) $form_display->getComponent('field_tva'));
|
||||||
|
$form_display->setComponent('field_repartition', ['weight' => 9] + (array) $form_display->getComponent('field_repartition'));
|
||||||
|
$form_display->setComponent('field_notes', ['weight' => 10] + (array) $form_display->getComponent('field_notes'));
|
||||||
|
$form_display->setComponent('field_flag', ['weight' => 11] + (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' => 8] + (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' => 9] + (array) $view_display->getComponent('field_repartition'));
|
||||||
|
$view_display->setComponent('field_notes', ['weight' => 10] + (array) $view_display->getComponent('field_notes'));
|
||||||
|
$view_display->setComponent('field_flag', ['weight' => 11] + (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).";
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ dashboard_compte:
|
|||||||
- core/drupal
|
- core/drupal
|
||||||
- figli_compta_ledger/vue
|
- figli_compta_ledger/vue
|
||||||
|
|
||||||
|
ledger_form:
|
||||||
|
js:
|
||||||
|
js/ledger-form.js: {}
|
||||||
|
css:
|
||||||
|
theme:
|
||||||
|
css/ledger-form.css: {}
|
||||||
|
dependencies:
|
||||||
|
- core/drupal
|
||||||
|
- core/once
|
||||||
|
|
||||||
admin_chrome:
|
admin_chrome:
|
||||||
css:
|
css:
|
||||||
theme:
|
theme:
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
|||||||
if (!in_array($form_id, ['node_ligne_comptable_form', 'node_ligne_comptable_edit_form'], TRUE)) {
|
if (!in_array($form_id, ['node_ligne_comptable_form', 'node_ligne_comptable_edit_form'], TRUE)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The add and edit forms get different auto-generated classes
|
||||||
|
// (node-ligne-comptable-form vs. node-ligne-comptable-edit-form) --
|
||||||
|
// one shared class so css/ledger-form.css can target both without
|
||||||
|
// duplicating every selector.
|
||||||
|
$form['#attributes']['class'][] = 'figli-ledger-form';
|
||||||
$form['#validate'][] = 'figli_compta_ledger_validate_repartition';
|
$form['#validate'][] = 'figli_compta_ledger_validate_repartition';
|
||||||
|
|
||||||
// Every save must create a revision, unconditionally -- there's no
|
// Every save must create a revision, unconditionally -- there's no
|
||||||
@@ -71,6 +76,123 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Title" is generic Drupal boilerplate that means nothing to a
|
||||||
|
// freelancer entering an accounting line -- but home.js/dashboard*.js
|
||||||
|
// do fall back to it as the displayed "Libellé" when Notes / détail is
|
||||||
|
// left empty, so it can't just be hidden. Relabel it so that
|
||||||
|
// relationship is explicit instead of a mystery required field.
|
||||||
|
if (isset($form['title']['widget'][0]['value'])) {
|
||||||
|
$form['title']['widget'][0]['value']['#title'] = t('Libellé court');
|
||||||
|
$form['title']['widget'][0]['value']['#description'] = t('Affiché dans le grand livre si "Notes / détail" ci-dessous est laissé vide.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu settings / URL alias / Authoring information: none of this is
|
||||||
|
// ever used for a ligne_comptable (no menu links, no path aliases, and
|
||||||
|
// the acting user + revision timestamp are already captured by the
|
||||||
|
// forced revision in figli_compta_ledger_node_presave() below) -- just
|
||||||
|
// noise in a form meant to be a quick numeric entry, not a full content
|
||||||
|
// editing screen. #access rather than deleting the keys: safe no-op if
|
||||||
|
// a future field ever gets #group => 'advanced'.
|
||||||
|
if (isset($form['advanced'])) {
|
||||||
|
$form['advanced']['#access'] = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Published" is meaningless here too -- there's no draft/unpublished
|
||||||
|
// workflow for ledger lines, every one of them is expected to count
|
||||||
|
// immediately. Same pattern as the 'revision' field above: force the
|
||||||
|
// value rather than exposing a checkbox nobody should ever touch.
|
||||||
|
if (isset($form['status'])) {
|
||||||
|
$form['status']['widget']['value']['#default_value'] = TRUE;
|
||||||
|
$form['status']['#access'] = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Montant TTC is computed from Montant HT + TVA (see
|
||||||
|
// figli_compta_ledger_node_presave()) -- genuinely not editable
|
||||||
|
// (#disabled, not just readonly: a disabled field can't be focused or
|
||||||
|
// tabbed into at all, and Form API discards whatever a tampered
|
||||||
|
// request submits for it in favor of #default_value, same protection
|
||||||
|
// already used for 'revision' above). js/ledger-form.js can still set
|
||||||
|
// its .value directly for the live preview -- #disabled only blocks
|
||||||
|
// user interaction, not script writes.
|
||||||
|
if (isset($form['field_montant_ttc']['widget'][0]['value'])) {
|
||||||
|
$form['field_montant_ttc']['widget'][0]['value']['#disabled'] = TRUE;
|
||||||
|
$form['field_montant_ttc']['widget'][0]['value']['#description'] = t('Calculé automatiquement à partir du montant HT et de la TVA.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TVA: a select of the official French rates covers the overwhelming
|
||||||
|
// majority of new lines with one click, instead of associates having
|
||||||
|
// to know/type "20" or "5.5" by heart. "Autre" reveals the real
|
||||||
|
// decimal field underneath for the rest -- crucially including every
|
||||||
|
// pre-migration line, whose backfilled rate (see
|
||||||
|
// figli_compta_ledger_update_8005()) is often a blended, non-standard
|
||||||
|
// percentage that wouldn't match any of these options, and must stay
|
||||||
|
// editable/visible as the exact figure it is rather than being forced
|
||||||
|
// to snap to the nearest official rate.
|
||||||
|
if (isset($form['field_tva']['widget'][0]['value'])) {
|
||||||
|
$taux_officiels = [
|
||||||
|
'0' => t('0 % (exonéré)'),
|
||||||
|
'2.1' => t('2,1 % (taux particulier)'),
|
||||||
|
'5.5' => t('5,5 % (taux réduit)'),
|
||||||
|
'10' => t('10 % (taux intermédiaire)'),
|
||||||
|
'20' => t('20 % (taux normal)'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$tva_field = &$form['field_tva']['widget'][0]['value'];
|
||||||
|
$current = $tva_field['#default_value'] ?? NULL;
|
||||||
|
$rate_default = 'autre';
|
||||||
|
if ($current === NULL || $current === '') {
|
||||||
|
// 10 % (taux intermédiaire), not 0 -- most new lines associates
|
||||||
|
// enter do carry VAT, so this saves a click on the common case
|
||||||
|
// instead of silently defaulting to "no VAT" for every line
|
||||||
|
// unless someone remembers to change it.
|
||||||
|
$rate_default = '10';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach (array_keys($taux_officiels) as $rate) {
|
||||||
|
if (abs((float) $current - (float) $rate) < 0.0001) {
|
||||||
|
$rate_default = $rate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plain top-level element, deliberately *not* nested inside
|
||||||
|
// $form['field_tva'] -- Field API's WidgetBase::extractFormValues()
|
||||||
|
// reads the entire form_state value at that path as the field's own
|
||||||
|
// delta-indexed value array, and chokes on an unrelated sibling key
|
||||||
|
// mixed into it.
|
||||||
|
//
|
||||||
|
// Weight 6: the exact slot Montant HT (5)/Montant TTC (7) leave open
|
||||||
|
// between them in figli_compta_ledger_update_8004() -- field_tva
|
||||||
|
// itself has been moved to weight 8 (its own full-width row) so
|
||||||
|
// that this select forms a clean HT/rate/TTC three-column row
|
||||||
|
// whether or not "Autre" is currently revealing the real field_tva
|
||||||
|
// input further down. See css/ledger-form.css's grid-column rules
|
||||||
|
// for both -- the row layout there depends on this exact ordering,
|
||||||
|
// not just on matching #weight values.
|
||||||
|
$form['field_tva_rate'] = [
|
||||||
|
'#type' => 'select',
|
||||||
|
'#title' => t('TVA'),
|
||||||
|
'#options' => $taux_officiels + ['autre' => t('Autre (préciser)')],
|
||||||
|
'#default_value' => $rate_default,
|
||||||
|
'#weight' => 6,
|
||||||
|
];
|
||||||
|
$tva_field['#title'] = t('Taux exact (%)');
|
||||||
|
// Not #states: this element sits outside the field system (see the
|
||||||
|
// comment above) and, tested in this exact AJAX-loaded modal,
|
||||||
|
// #states' data-drupal-states attribute gets attached correctly but
|
||||||
|
// never actually toggles visibility here -- unclear why (a working
|
||||||
|
// #states dependency already exists a few fields up, on
|
||||||
|
// field_entree_liee, watching a real Field API select rather than
|
||||||
|
// this manually-added one), not worth chasing further when
|
||||||
|
// js/ledger-form.js already has to run custom JS on this form
|
||||||
|
// anyway for the HT/TVA -> TTC live preview. Plain JS toggle there
|
||||||
|
// instead -- see figliLedgerTvaRateToggle.
|
||||||
|
$form['#validate'][] = 'figli_compta_ledger_apply_tva_rate';
|
||||||
|
}
|
||||||
|
|
||||||
|
$form['#attached']['library'][] = 'figli_compta_ledger/ledger_form';
|
||||||
|
|
||||||
$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)
|
||||||
@@ -115,6 +237,28 @@ function figli_compta_ledger_validate_repartition(array &$form, FormStateInterfa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form #validate callback: when a standard French VAT rate was picked from
|
||||||
|
* the field_tva_rate select (added in figli_compta_ledger_form_alter()),
|
||||||
|
* copies it into field_tva's own submitted value -- that's the field
|
||||||
|
* figli_compta_ledger_node_presave() actually reads to compute Montant TTC.
|
||||||
|
* Left untouched when "Autre" is selected: the real field_tva input,
|
||||||
|
* shown in that case by js/ledger-form.js's figliLedgerTvaRateToggle,
|
||||||
|
* already carries whatever the associate typed there.
|
||||||
|
*
|
||||||
|
* Runs late enough to matter despite EntityForm::validateForm() building an
|
||||||
|
* interim entity earlier in the same validate phase (for constraint
|
||||||
|
* checking only) -- submitForm() rebuilds the entity fresh from
|
||||||
|
* $form_state's values right before save(), by which point every
|
||||||
|
* #validate callback, this one included, has already run.
|
||||||
|
*/
|
||||||
|
function figli_compta_ledger_apply_tva_rate(array &$form, FormStateInterface $form_state) {
|
||||||
|
$rate = $form_state->getValue('field_tva_rate');
|
||||||
|
if ($rate !== NULL && $rate !== 'autre') {
|
||||||
|
$form_state->setValue(['field_tva', 0, 'value'], $rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #ajax callback for the node form submit button: close the modal on
|
* #ajax callback for the node form submit button: close the modal on
|
||||||
* success, or re-render the form (with errors) in place on failure.
|
* success, or re-render the form (with errors) in place on failure.
|
||||||
@@ -163,8 +307,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') {
|
||||||
@@ -184,7 +329,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)) {
|
||||||
@@ -192,6 +337,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,108 @@
|
|||||||
|
/**
|
||||||
|
* @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 rate = form.querySelector('[name="field_tva_rate"]');
|
||||||
|
var ttc = form.querySelector('[name="field_montant_ttc[0][value]"]');
|
||||||
|
if (!tva || !ttc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rate select (when not "Autre") is the actual source of
|
||||||
|
// truth here -- figli_compta_ledger_apply_tva_rate() copies its
|
||||||
|
// value into field_tva server-side on submit, but that happens
|
||||||
|
// too late to matter for a live client-side preview.
|
||||||
|
function currentTva() {
|
||||||
|
if (rate && rate.value !== 'autre') {
|
||||||
|
return parseFloat(rate.value);
|
||||||
|
}
|
||||||
|
return parseFloat(tva.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recompute() {
|
||||||
|
var htValue = parseFloat(ht.value);
|
||||||
|
if (isNaN(htValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var tvaValue = currentTva();
|
||||||
|
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,
|
||||||
|
// TVA, or the rate select are actually edited, matching the
|
||||||
|
// same "only overwrite Montant TTC when HT/TVA actually
|
||||||
|
// changed" guard in figli_compta_ledger_node_presave(). Harmless
|
||||||
|
// no-op on a genuinely new/blank line either way (HT is empty,
|
||||||
|
// recompute() bails out above).
|
||||||
|
ht.addEventListener('input', recompute);
|
||||||
|
tva.addEventListener('input', recompute);
|
||||||
|
if (rate) {
|
||||||
|
rate.addEventListener('change', recompute);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows/hides the real "Taux exact (%)" field_tva input based on the
|
||||||
|
* field_tva_rate select added in figli_compta_ledger_form_alter() --
|
||||||
|
* plain JS rather than #states: tested and confirmed #states' own
|
||||||
|
* data-drupal-states attribute gets attached correctly here but never
|
||||||
|
* actually toggles visibility, for reasons not tracked down (a
|
||||||
|
* working #states dependency already exists a few fields up, on
|
||||||
|
* field_entree_liee, watching a real Field API element rather than
|
||||||
|
* this manually-added select -- that one difference is the only lead
|
||||||
|
* so far). Not worth blocking on since this form already needs custom
|
||||||
|
* JS anyway for the TTC live preview above.
|
||||||
|
*/
|
||||||
|
Drupal.behaviors.figliLedgerTvaRateToggle = {
|
||||||
|
attach: function (context) {
|
||||||
|
once('figli-ledger-tva-rate-toggle', '[name="field_tva_rate"]', context).forEach(function (select) {
|
||||||
|
var form = select.closest('form');
|
||||||
|
var exact = form.querySelector('.field--name-field-tva');
|
||||||
|
if (!exact) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
exact.style.display = select.value === 'autre' ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', toggle);
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(Drupal, once);
|
||||||
Reference in New Issue
Block a user