Compare commits
6
Commits
897d3c208e
...
00956825fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00956825fa | ||
|
|
74ee8ab3b9 | ||
|
|
6576b35c07 | ||
|
|
0baa03855b | ||
|
|
6c33bc0098 | ||
|
|
134c97ac9d |
@@ -4,6 +4,8 @@ status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.ligne_comptable.field_client
|
||||
- field.field.node.ligne_comptable.field_cotisation_active
|
||||
- field.field.node.ligne_comptable.field_cotisation_urssaf
|
||||
- field.field.node.ligne_comptable.field_date_ligne
|
||||
- field.field.node.ligne_comptable.field_entree_liee
|
||||
- field.field.node.ligne_comptable.field_flag
|
||||
@@ -40,6 +42,20 @@ content:
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_cotisation_active:
|
||||
type: boolean_checkbox
|
||||
weight: 4
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
field_cotisation_urssaf:
|
||||
type: number
|
||||
weight: 6
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_date_ligne:
|
||||
type: datetime_default
|
||||
weight: 0
|
||||
@@ -58,7 +74,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_flag:
|
||||
type: entity_reference_autocomplete_tags
|
||||
weight: 11
|
||||
weight: 12
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
@@ -75,14 +91,14 @@ content:
|
||||
third_party_settings: { }
|
||||
field_montant_ttc:
|
||||
type: number
|
||||
weight: 7
|
||||
weight: 8
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_notes:
|
||||
type: string_textarea
|
||||
weight: 10
|
||||
weight: 11
|
||||
region: content
|
||||
settings:
|
||||
rows: 5
|
||||
@@ -98,7 +114,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_repartition:
|
||||
type: paragraphs
|
||||
weight: 9
|
||||
weight: 10
|
||||
region: content
|
||||
settings:
|
||||
title: Répartition
|
||||
@@ -116,7 +132,7 @@ content:
|
||||
third_party_settings: { }
|
||||
field_tva:
|
||||
type: number
|
||||
weight: 8
|
||||
weight: 9
|
||||
region: content
|
||||
settings:
|
||||
placeholder: ''
|
||||
|
||||
@@ -4,6 +4,8 @@ status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.ligne_comptable.field_client
|
||||
- field.field.node.ligne_comptable.field_cotisation_active
|
||||
- field.field.node.ligne_comptable.field_cotisation_urssaf
|
||||
- field.field.node.ligne_comptable.field_date_ligne
|
||||
- field.field.node.ligne_comptable.field_entree_liee
|
||||
- field.field.node.ligne_comptable.field_flag
|
||||
@@ -33,6 +35,27 @@ content:
|
||||
third_party_settings: { }
|
||||
weight: 2
|
||||
region: content
|
||||
field_cotisation_active:
|
||||
type: boolean
|
||||
label: above
|
||||
settings:
|
||||
format: default
|
||||
format_custom_false: ''
|
||||
format_custom_true: ''
|
||||
third_party_settings: { }
|
||||
weight: 4
|
||||
region: content
|
||||
field_cotisation_urssaf:
|
||||
type: number_decimal
|
||||
label: above
|
||||
settings:
|
||||
thousand_separator: ''
|
||||
decimal_separator: .
|
||||
scale: 2
|
||||
prefix_suffix: true
|
||||
third_party_settings: { }
|
||||
weight: 6
|
||||
region: content
|
||||
field_date_ligne:
|
||||
type: datetime_default
|
||||
label: above
|
||||
@@ -56,7 +79,7 @@ content:
|
||||
settings:
|
||||
link: true
|
||||
third_party_settings: { }
|
||||
weight: 11
|
||||
weight: 12
|
||||
region: content
|
||||
field_montant_ht:
|
||||
type: number_decimal
|
||||
@@ -78,14 +101,14 @@ content:
|
||||
scale: 2
|
||||
prefix_suffix: true
|
||||
third_party_settings: { }
|
||||
weight: 7
|
||||
weight: 8
|
||||
region: content
|
||||
field_notes:
|
||||
type: basic_string
|
||||
label: above
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 10
|
||||
weight: 11
|
||||
region: content
|
||||
field_numero_facture:
|
||||
type: string
|
||||
@@ -103,7 +126,7 @@ content:
|
||||
view_mode: default
|
||||
link: ''
|
||||
third_party_settings: { }
|
||||
weight: 9
|
||||
weight: 10
|
||||
region: content
|
||||
field_tva:
|
||||
type: number_decimal
|
||||
@@ -114,7 +137,7 @@ content:
|
||||
scale: 2
|
||||
prefix_suffix: true
|
||||
third_party_settings: { }
|
||||
weight: 8
|
||||
weight: 9
|
||||
region: content
|
||||
field_type_ligne:
|
||||
type: list_default
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
uuid: 3defc3d6-42b6-4340-a7cf-fecadb919934
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_cotisation_active
|
||||
- node.type.ligne_comptable
|
||||
id: node.ligne_comptable.field_cotisation_active
|
||||
field_name: field_cotisation_active
|
||||
entity_type: node
|
||||
bundle: ligne_comptable
|
||||
label: 'Cotisation diffuseur URSSAF (1,1%)'
|
||||
description: ''
|
||||
required: false
|
||||
translatable: true
|
||||
default_value:
|
||||
-
|
||||
value: 1
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
on_label: 'On'
|
||||
off_label: 'Off'
|
||||
field_type: boolean
|
||||
@@ -0,0 +1,23 @@
|
||||
uuid: 309c0b94-be32-42a3-b990-cb04858afb8d
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_cotisation_urssaf
|
||||
- node.type.ligne_comptable
|
||||
id: node.ligne_comptable.field_cotisation_urssaf
|
||||
field_name: field_cotisation_urssaf
|
||||
entity_type: node
|
||||
bundle: ligne_comptable
|
||||
label: '1,1%'
|
||||
description: ''
|
||||
required: false
|
||||
translatable: true
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
min: null
|
||||
max: null
|
||||
prefix: ''
|
||||
suffix: ''
|
||||
field_type: decimal
|
||||
@@ -0,0 +1,18 @@
|
||||
uuid: 28c4ae5a-011c-4f31-bca5-36874c6c182b
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_cotisation_active
|
||||
field_name: field_cotisation_active
|
||||
entity_type: node
|
||||
type: boolean
|
||||
settings: { }
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
@@ -0,0 +1,20 @@
|
||||
uuid: 486554c0-7686-4c52-ab33-eb09b5573be5
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_cotisation_urssaf
|
||||
field_name: field_cotisation_urssaf
|
||||
entity_type: node
|
||||
type: decimal
|
||||
settings:
|
||||
precision: 12
|
||||
scale: 2
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
@@ -2,12 +2,28 @@
|
||||
on those two custom routes, but attached here rather than duplicated
|
||||
between home.css and dashboard.css -- and defined with its own explicit
|
||||
colors rather than relying on either page's #figli-*-app-scoped custom
|
||||
properties, since this <nav> sits outside both of those elements. */
|
||||
properties, since this <nav> sits outside both of those elements.
|
||||
|
||||
Fixed rather than in normal flow: this <nav> and the page's <h1>
|
||||
("Grand livre - SAS Figures Libres" etc.) come from two different
|
||||
Drupal block regions (the title from Gin's sticky top-bar header,
|
||||
this nav from the page content below it) with no shared flex/grid
|
||||
container to align them side by side declaratively -- in normal flow
|
||||
it rendered as its own full-width row *under* the title, costing
|
||||
~75px of vertical space the table below could otherwise use before
|
||||
the whole page (not just the table's own internal scroll) needed to
|
||||
scroll too. Pulling it out of flow and pinning it next to the title
|
||||
reclaims that space; top/right below are tuned to that title's actual
|
||||
position, not derived from it, so they'd need revisiting if Gin's
|
||||
header height or padding ever changes. */
|
||||
.figli-page-nav {
|
||||
position: fixed;
|
||||
top: 0.65rem;
|
||||
right: 1rem;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.25rem 0;
|
||||
}
|
||||
.figli-page-nav a {
|
||||
padding: 0.3rem 0.8rem;
|
||||
|
||||
@@ -26,3 +26,22 @@
|
||||
.dialog-off-canvas-main-canvas {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
/* Gin's default 80px margin-bottom on <main>, meant as breathing room
|
||||
below a normal admin content page -- on this app's tight,
|
||||
single-viewport layout (page nav pinned next to the title, table
|
||||
scrolling internally, see .figli-page-nav in admin-chrome.css) it was
|
||||
the last remaining chunk of an *outer* page scroll on top of the
|
||||
table's own, since max-height: 75vh on .figli-table-wrap already
|
||||
accounts for everything else above/below it but not this. */
|
||||
main.page-content {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Gin's default 48px left/right margin on .layout-container -- fine for
|
||||
a normal admin content page with a narrower reading measure, but this
|
||||
app's table wants the width. Cut to a third (16px). */
|
||||
.layout-container {
|
||||
margin-left: 16px !important;
|
||||
margin-right: 16px !important;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
--figli-col-hover: rgba(15, 23, 42, 0.05);
|
||||
|
||||
font-family: Inter, -apple-system, sans-serif;
|
||||
margin: 1rem 0;
|
||||
/* Bottom cut to a third (top left alone) -- with the outer page
|
||||
scroll already eliminated (see hide-admin-chrome.css's main.page-
|
||||
content override), the full 1rem below the table was just dead
|
||||
space past the last visible row. */
|
||||
margin: 1rem 0 0.33rem;
|
||||
color: var(--figli-text);
|
||||
}
|
||||
|
||||
@@ -172,7 +176,15 @@ html.gin--dark-mode #figli-home-app {
|
||||
overflow-x: hidden;
|
||||
border: 1px solid var(--figli-border);
|
||||
border-radius: 6px;
|
||||
max-height: 75vh;
|
||||
/* Not a flat 75vh -- title/nav + toolbar (with its filter row) above
|
||||
this add up to a roughly fixed ~16rem regardless of viewport
|
||||
height, so a percentage-of-viewport max-height only fits by
|
||||
coincidence at one particular window size and silently reintroduces
|
||||
the outer page scroll (on top of this element's own) at any other,
|
||||
as toolbar filters were added over time and pushed that fixed part
|
||||
past what 75vh left room for. Subtracting the fixed part directly
|
||||
keeps this sized to exactly the remaining space instead. */
|
||||
max-height: calc(100vh - 16rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -292,12 +304,21 @@ html.gin--dark-mode #figli-home-app {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Écart is called out by the Écart column itself (bold red, see
|
||||
.figli-ecart below) -- no separate row-level treatment needed. */
|
||||
/* Écart is called out by the Écart column itself (bold, red or green
|
||||
depending on sign -- see below) -- no separate row-level treatment
|
||||
needed. Répartition > Montant HT (positive écart) means the accounts
|
||||
were credited more than what's actually owed for this line, the same
|
||||
"more money than expected" sense .figli-montant-positif already
|
||||
marks green elsewhere; the reverse (negative) stays the usual
|
||||
red/error color. */
|
||||
#figli-home-app td.figli-ecart {
|
||||
color: var(--figli-error);
|
||||
font-weight: 700;
|
||||
}
|
||||
#figli-home-app td.figli-ecart-positif {
|
||||
color: var(--figli-positive);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Signalé rows: a thick amber accent bar on the left edge. */
|
||||
#figli-home-app tr.figli-flag-row td:first-child {
|
||||
@@ -457,9 +478,26 @@ html.gin--dark-mode #figli-home-app {
|
||||
#figli-home-app .amount.figli-ttc-col {
|
||||
width: 5%;
|
||||
}
|
||||
/* Cotisation diffuseur URSSAF (1,1%) -- blank for every non-"Entrée
|
||||
client" row (see figli_compta_ledger_node_presave()), same width
|
||||
budget as HT/TTC since it's a real € amount, not a short rate like
|
||||
TVA. */
|
||||
#figli-home-app .amount.figli-cotisation-col {
|
||||
width: 5%;
|
||||
}
|
||||
#figli-home-app .amount.figli-tva-col {
|
||||
width: 3.5%;
|
||||
}
|
||||
/* Non-official TVA rate marker -- a thick red underline on the cell
|
||||
itself rather than a small badge/dot (tried first, not visible
|
||||
enough): the column is only ~3.5% wide, barely enough for
|
||||
"12,64 %" itself, so a border under the whole cell reads clearly at
|
||||
a glance without needing extra horizontal room a text/icon badge
|
||||
would need. Tooltip (title attribute in the twig template) carries
|
||||
the actual explanation. */
|
||||
#figli-home-app td.figli-tva-nonofficial {
|
||||
border-bottom: 3px solid var(--figli-error);
|
||||
}
|
||||
#figli-home-app .amount.compte-col {
|
||||
width: 4.3%;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
--flform-danger: #b3261e;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem 1rem;
|
||||
max-width: 44rem;
|
||||
margin: 0 auto;
|
||||
@@ -59,15 +59,18 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
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(). */
|
||||
/* Row layout, fully explicit (not left to grid auto-placement) -- with
|
||||
Cotisation now in the mix there are three separate row groupings
|
||||
(Client + N° Facture, the checkbox on its own full-width row, then
|
||||
Montant HT + Cotisation + TVA + Montant TTC as a clean four-column
|
||||
row) and relying on DOM order alone to keep them from bleeding into
|
||||
each other got fragile once that many financial fields entered the
|
||||
picture. This must stay in sync with the field weights set in
|
||||
figli_compta_ledger_update_8006()/_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-cotisation-active,
|
||||
.figli-ledger-form > .field--name-field-tva,
|
||||
.figli-ledger-form > .field--name-field-repartition,
|
||||
.figli-ledger-form > .field--name-field-notes,
|
||||
.figli-ledger-form > .field--name-field-flag {
|
||||
@@ -77,31 +80,32 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
.figli-ledger-form > .field--name-field-type-ligne {
|
||||
grid-column: 2 / 4;
|
||||
grid-column: 2 / 5;
|
||||
}
|
||||
.figli-ledger-form > .field--name-field-client {
|
||||
grid-column: 1 / 3;
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
.figli-ledger-form > .field--name-field-numero-facture {
|
||||
grid-column: 3 / 4;
|
||||
grid-column: 4 / 5;
|
||||
}
|
||||
.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 {
|
||||
.figli-ledger-form > .field--name-field-cotisation-urssaf {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
.figli-ledger-form > .field--name-field-tva {
|
||||
grid-column: 1 / -1;
|
||||
/* 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 third
|
||||
slot of the HT/Cotisation/TVA/TTC row -- see that function's #weight
|
||||
comment for why this specific ordering (weight 7, between Cotisation's
|
||||
6 and TTC's 8) matters, not just matching column numbers. field_tva
|
||||
itself (grouped with the other always-full-width fields above) only
|
||||
appears -- full width, its own row -- when "Autre" is picked. */
|
||||
.figli-ledger-form > .js-form-item-field-tva-rate {
|
||||
grid-column: 3 / 4;
|
||||
}
|
||||
.figli-ledger-form > .field--name-field-montant-ttc {
|
||||
grid-column: 3 / 4;
|
||||
grid-column: 4 / 5;
|
||||
}
|
||||
|
||||
/* Field basics */
|
||||
|
||||
@@ -218,6 +218,13 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
|
||||
// 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]);
|
||||
// Cotisation diffuseur URSSAF (1,1%), entrée client lines only -- see
|
||||
// figli_compta_ledger_update_8006()'s docblock for the full
|
||||
// explanation. field_cotisation_active is the editable on/off toggle
|
||||
// (a devis doesn't always include it); field_cotisation_urssaf is the
|
||||
// computed amount (Montant HT * 1.011), never entered by hand.
|
||||
_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]);
|
||||
_figli_field('node', 'ligne_comptable', 'field_notes', 'Notes / détail', 'string_long');
|
||||
|
||||
@@ -242,19 +249,30 @@ 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])
|
||||
// Cotisation diffuseur URSSAF (1,1%) -- only "Entrée client" lines
|
||||
// go through a client devis, the only place this cotisation is
|
||||
// ever invoiced (see figli_compta_ledger_update_8006()'s
|
||||
// docblock). Optional even there: not every entrée historically
|
||||
// included it, so a checkbox rather than an automatic
|
||||
// type-based assumption. Weight 4, tied with field_entree_liee --
|
||||
// the two types that show each are mutually exclusive
|
||||
// (entrée vs. versement/achat/hébergement/sous_traitant), so
|
||||
// which one wins the tie never matters.
|
||||
->setComponent('field_cotisation_active', ['type' => 'boolean_checkbox', 'weight' => 4, 'settings' => ['display_label' => TRUE]])
|
||||
->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 5])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 7])
|
||||
// field_tva sits *after* Montant TTC, not between HT and TTC --
|
||||
// figli_compta_ledger_form_alter() inserts a non-field
|
||||
// "field_tva_rate" select at weight 6 (a select of the official
|
||||
->setComponent('field_cotisation_urssaf', ['type' => 'number', 'weight' => 6])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 8])
|
||||
// field_tva sits *after* Montant TTC, not between Cotisation and
|
||||
// TTC -- figli_compta_ledger_form_alter() inserts a non-field
|
||||
// "field_tva_rate" select at weight 7 (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])
|
||||
->setComponent('field_tva', ['type' => 'number', 'weight' => 9])
|
||||
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 10, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
|
||||
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 11])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 12])
|
||||
->save();
|
||||
}
|
||||
|
||||
@@ -269,12 +287,14 @@ 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_cotisation_active', ['type' => 'boolean', 'weight' => 4])
|
||||
->setComponent('field_montant_ht', ['type' => 'number_decimal', 'weight' => 5])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 7])
|
||||
->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 8])
|
||||
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 9])
|
||||
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 10])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 11])
|
||||
->setComponent('field_cotisation_urssaf', ['type' => 'number_decimal', 'weight' => 6])
|
||||
->setComponent('field_montant_ttc', ['type' => 'number_decimal', 'weight' => 8])
|
||||
->setComponent('field_tva', ['type' => 'number_decimal', 'weight' => 9])
|
||||
->setComponent('field_repartition', ['type' => 'entity_reference_revisions_entity_view', 'weight' => 10])
|
||||
->setComponent('field_notes', ['type' => 'basic_string', 'weight' => 11])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_label', 'weight' => 12])
|
||||
->save();
|
||||
}
|
||||
}
|
||||
@@ -493,3 +513,213 @@ function figli_compta_ledger_update_8005() {
|
||||
|
||||
return "TVA calculée pour $filled lignes, $skipped laissées vides (pas de Montant TTC renseigné ou Montant HT nul).";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the cotisation diffuseur URSSAF (1,1%) fields to ligne_comptable,
|
||||
* and switches Montant TTC's computation for "Entrée client" lines when
|
||||
* the cotisation applies: TTC = round(cotisation * (1 + TVA/100), 2)
|
||||
* instead of straight from Montant HT.
|
||||
*
|
||||
* The SAS invoices clients a HT amount, then -- when the devis includes
|
||||
* it, not always -- adds a 1,1% "cotisation diffuseur" the SAS owes
|
||||
* URSSAF for using freelancers, *then* applies TVA on top of that
|
||||
* increased amount, not on the raw HT. Only "Entrée client" lines ever
|
||||
* go through a client devis at all; every other type (versement, achat,
|
||||
* charge...) is an outgoing/internal line with nothing to invoice a
|
||||
* cotisation on, so both fields below are #states-hidden outside that
|
||||
* type (see figli_compta_ledger_form_alter()) and
|
||||
* figli_compta_ledger_node_presave() never applies the cascade to them.
|
||||
*
|
||||
* field_tva as backfilled by figli_compta_ledger_update_8005() didn't
|
||||
* know about this cascade and is wrong for every entrée line that did
|
||||
* have the cotisation applied (it computed straight from HT, so what it
|
||||
* stored is actually the *blended* HT->TTC rate, not the real TVA
|
||||
* rate) -- corrected separately in figli_compta_ledger_update_8007(),
|
||||
* which also backfills field_cotisation_active/field_cotisation_urssaf
|
||||
* for existing content.
|
||||
*/
|
||||
function figli_compta_ledger_update_8006() {
|
||||
_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]);
|
||||
|
||||
// New entrée lines default to the cotisation being applied -- the
|
||||
// associates' normal practice going forward, even though it was only
|
||||
// roughly half of existing content (figli_compta_ledger_update_8007()
|
||||
// detects each historical line on its own rather than assuming either
|
||||
// way for what's already there).
|
||||
$field_active = FieldConfig::loadByName('node', 'ligne_comptable', 'field_cotisation_active');
|
||||
if ($field_active && $field_active->getDefaultValueLiteral() === []) {
|
||||
$field_active->setDefaultValue(1)->save();
|
||||
}
|
||||
|
||||
// See figli_compta_ledger_update_8004()'s comment on why every weight
|
||||
// here is a plain integer (fractional weights silently truncate).
|
||||
$form_display = EntityFormDisplay::load('node.ligne_comptable.default');
|
||||
if ($form_display) {
|
||||
$form_display->setComponent('field_cotisation_active', ['type' => 'boolean_checkbox', 'weight' => 4, 'settings' => ['display_label' => TRUE]]);
|
||||
$form_display->setComponent('field_montant_ht', ['type' => 'number', 'weight' => 5] + (array) $form_display->getComponent('field_montant_ht'));
|
||||
$form_display->setComponent('field_cotisation_urssaf', ['type' => 'number', 'weight' => 6]);
|
||||
// Weight 7 (between Cotisation and Montant TTC) is the non-field
|
||||
// "field_tva_rate" select figli_compta_ledger_form_alter() adds.
|
||||
$form_display->setComponent('field_montant_ttc', ['type' => 'number', 'weight' => 8] + (array) $form_display->getComponent('field_montant_ttc'));
|
||||
$form_display->setComponent('field_tva', ['type' => 'number', 'weight' => 9] + (array) $form_display->getComponent('field_tva'));
|
||||
$form_display->setComponent('field_repartition', ['weight' => 10] + (array) $form_display->getComponent('field_repartition'));
|
||||
$form_display->setComponent('field_notes', ['weight' => 11] + (array) $form_display->getComponent('field_notes'));
|
||||
$form_display->setComponent('field_flag', ['weight' => 12] + (array) $form_display->getComponent('field_flag'));
|
||||
$form_display->save();
|
||||
}
|
||||
|
||||
$view_display = EntityViewDisplay::load('node.ligne_comptable.default');
|
||||
if ($view_display) {
|
||||
$view_display->setComponent('field_cotisation_active', ['type' => 'boolean', 'weight' => 4]);
|
||||
$view_display->setComponent('field_cotisation_urssaf', ['type' => 'number_decimal', 'weight' => 6]);
|
||||
$view_display->setComponent('field_montant_ttc', ['weight' => 8] + (array) $view_display->getComponent('field_montant_ttc'));
|
||||
$view_display->setComponent('field_tva', ['weight' => 9] + (array) $view_display->getComponent('field_tva'));
|
||||
$view_display->setComponent('field_repartition', ['weight' => 10] + (array) $view_display->getComponent('field_repartition'));
|
||||
$view_display->setComponent('field_notes', ['weight' => 11] + (array) $view_display->getComponent('field_notes'));
|
||||
$view_display->setComponent('field_flag', ['weight' => 12] + (array) $view_display->getComponent('field_flag'));
|
||||
$view_display->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfills field_cotisation_active/field_cotisation_urssaf and
|
||||
* corrects field_tva for existing "Entrée client" lines, now that the
|
||||
* HT -> +1,1% -> +TVA cascade explained in
|
||||
* figli_compta_ledger_update_8006()'s docblock is understood. 2021 is
|
||||
* left untouched entirely (explicit instruction -- that year's
|
||||
* cotisation practice isn't confirmed), and Montant TTC itself is never
|
||||
* written here, same "never correct historical data" rule as every
|
||||
* other migration in this module.
|
||||
*
|
||||
* Per entrée line (2022+, both Montant HT and Montant TTC present):
|
||||
* - old_rate = (TTC/HT - 1) * 100 -- what
|
||||
* figli_compta_ledger_update_8005() already stored in field_tva.
|
||||
* - If old_rate already lands on an official French VAT rate (0, 2.1,
|
||||
* 5.5, 10, 20 -- ±0.05 points, allowing for rounding through 2-decimal
|
||||
* HT/TTC storage), the cotisation was never applied to this line:
|
||||
* field_tva is already correct as migrated, left alone, cotisation
|
||||
* marked inactive.
|
||||
* - Otherwise, new_rate = (TTC / (HT * 1.011) - 1) * 100. If *that*
|
||||
* lands on an official rate, the cotisation was applied and old_rate
|
||||
* was the wrong (blended HT->TTC) figure all along -- field_tva
|
||||
* corrected to new_rate, cotisation marked active, field_cotisation_
|
||||
* urssaf backfilled to HT * 1.011.
|
||||
* - If neither lands cleanly, this is an ambiguous/blended case (a
|
||||
* handful of genuinely multi-rate invoices collapsed into one line,
|
||||
* same category figli_compta_ledger_update_8005() already found at 4-
|
||||
* decimal precision) -- field_tva left untouched, cotisation marked
|
||||
* inactive, rather than guess.
|
||||
*
|
||||
* Checked empirically before writing this (287 entrée lines with both
|
||||
* amounts, 2022+): 90 already official (left alone), 154 corrected, 40
|
||||
* still ambiguous both ways (left alone), 3 missing HT or TTC.
|
||||
*/
|
||||
function figli_compta_ledger_update_8007() {
|
||||
$official = [0.0, 2.1, 5.5, 10.0, 20.0];
|
||||
$epsilon = 0.05;
|
||||
$matches_official = function ($rate) use ($official, $epsilon) {
|
||||
foreach ($official as $o) {
|
||||
if (abs($rate - $o) < $epsilon) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
};
|
||||
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$nids = $storage->getQuery()
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'ligne_comptable')
|
||||
->condition('field_type_ligne', 'entree')
|
||||
->execute();
|
||||
|
||||
$skipped2021 = 0;
|
||||
$noData = 0;
|
||||
$alreadyOfficial = 0;
|
||||
$corrected = 0;
|
||||
$stillAmbiguous = 0;
|
||||
|
||||
\Drupal::state()->set('figli_compta_ledger.skip_validation', TRUE);
|
||||
foreach ($storage->loadMultiple($nids) as $node) {
|
||||
$date = $node->hasField('field_date_ligne') ? $node->get('field_date_ligne')->value : NULL;
|
||||
if ($date && substr($date, 0, 4) === '2021') {
|
||||
$skipped2021++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ht = $node->hasField('field_montant_ht') && !$node->get('field_montant_ht')->isEmpty()
|
||||
? (float) $node->get('field_montant_ht')->value : NULL;
|
||||
$ttc = $node->hasField('field_montant_ttc') && !$node->get('field_montant_ttc')->isEmpty()
|
||||
? (float) $node->get('field_montant_ttc')->value : NULL;
|
||||
if ($ht === NULL || $ttc === NULL || abs($ht) < 0.0001) {
|
||||
$node->set('field_cotisation_active', FALSE);
|
||||
$node->save();
|
||||
$noData++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$old_rate = round(($ttc / $ht - 1) * 100, 4);
|
||||
if ($matches_official($old_rate)) {
|
||||
$node->set('field_cotisation_active', FALSE);
|
||||
$node->save();
|
||||
$alreadyOfficial++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_rate = round(($ttc / ($ht * 1.011) - 1) * 100, 4);
|
||||
if (!$matches_official($new_rate)) {
|
||||
$node->set('field_cotisation_active', FALSE);
|
||||
$node->save();
|
||||
$stillAmbiguous++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$node->set('field_cotisation_active', TRUE);
|
||||
$node->set('field_cotisation_urssaf', round($ht * 1.011, 2));
|
||||
$node->set('field_tva', $new_rate);
|
||||
$node->save();
|
||||
$corrected++;
|
||||
}
|
||||
\Drupal::state()->delete('figli_compta_ledger.skip_validation');
|
||||
|
||||
return "Cotisation/TVA : $corrected lignes corrigées (cotisation active), $alreadyOfficial déjà correctes (cotisation inactive), $stillAmbiguous laissées telles quelles (taux non standard des deux façons), $noData sans HT/TTC, $skipped2021 lignes 2021 ignorées.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects what field_cotisation_urssaf stores for the lines
|
||||
* figli_compta_ledger_update_8007() marked cotisation-active: that
|
||||
* migration (and figli_compta_ledger_node_presave() as first written)
|
||||
* stored Montant HT + 1,1% there, but "1,1%" as a field/column label
|
||||
* means just the 1,1% itself -- the delta, not the augmented total.
|
||||
* TVA still applies to the augmented base (Montant HT + this field), so
|
||||
* Montant TTC is untouched by this correction; only
|
||||
* field_cotisation_urssaf's own value changes, from round(HT * 1.011, 2)
|
||||
* to round(HT * 0.011, 2). Safe to run unconditionally on every
|
||||
* cotisation-active line -- this is a bug in code from earlier the same
|
||||
* day, not a judgment call about ambiguous historical data like
|
||||
* figli_compta_ledger_update_8007()'s.
|
||||
*/
|
||||
function figli_compta_ledger_update_8008() {
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$nids = $storage->getQuery()
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'ligne_comptable')
|
||||
->condition('field_cotisation_active', 1)
|
||||
->execute();
|
||||
|
||||
$fixed = 0;
|
||||
\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()) {
|
||||
continue;
|
||||
}
|
||||
$ht = (float) $node->get('field_montant_ht')->value;
|
||||
$node->set('field_cotisation_urssaf', round($ht * 0.011, 2));
|
||||
$node->save();
|
||||
$fixed++;
|
||||
}
|
||||
\Drupal::state()->delete('figli_compta_ledger.skip_validation');
|
||||
|
||||
return "field_cotisation_urssaf corrigé (delta seul, pas HT + delta) pour $fixed lignes.";
|
||||
}
|
||||
|
||||
@@ -119,6 +119,27 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
||||
$form['field_montant_ttc']['widget'][0]['value']['#description'] = t('Calculé automatiquement à partir du montant HT et de la TVA.');
|
||||
}
|
||||
|
||||
// Cotisation diffuseur URSSAF (1,1%) -- only ever relevant for
|
||||
// "Entrée client" lines (the only type invoiced to a client via a
|
||||
// devis; see figli_compta_ledger_update_8006()'s docblock), so both
|
||||
// fields are hidden otherwise. Real #states on real fields watching
|
||||
// the real field_type_ligne select -- unlike field_tva_rate below,
|
||||
// this is the same working pattern already used a few fields up on
|
||||
// field_entree_liee, not the custom select that #states silently
|
||||
// failed to toggle.
|
||||
if (isset($form['field_cotisation_active'])) {
|
||||
$form['field_cotisation_active']['#states'] = [
|
||||
'visible' => [':input[name="field_type_ligne"]' => ['value' => 'entree']],
|
||||
];
|
||||
}
|
||||
if (isset($form['field_cotisation_urssaf']['widget'][0]['value'])) {
|
||||
$form['field_cotisation_urssaf']['#states'] = [
|
||||
'visible' => [':input[name="field_type_ligne"]' => ['value' => 'entree']],
|
||||
];
|
||||
$form['field_cotisation_urssaf']['widget'][0]['value']['#disabled'] = TRUE;
|
||||
$form['field_cotisation_urssaf']['widget'][0]['value']['#description'] = t('Calculé automatiquement (1,1% du montant HT) si la cotisation ci-dessus est cochée. La TVA est ensuite appliquée sur Montant HT + ce montant.');
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -162,10 +183,10 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
||||
// 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
|
||||
// Weight 7: the slot between field_cotisation_urssaf (6) and Montant
|
||||
// TTC (8) left open in figli_compta_ledger_update_8006() -- field_tva
|
||||
// itself sits at weight 9 (its own full-width row) so that this
|
||||
// select forms a clean HT/Cotisation/rate/TTC four-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,
|
||||
@@ -175,7 +196,7 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
||||
'#title' => t('TVA'),
|
||||
'#options' => $taux_officiels + ['autre' => t('Autre (préciser)')],
|
||||
'#default_value' => $rate_default,
|
||||
'#weight' => 6,
|
||||
'#weight' => 7,
|
||||
];
|
||||
$tva_field['#title'] = t('Taux exact (%)');
|
||||
// Not #states: this element sits outside the field system (see the
|
||||
@@ -338,30 +359,63 @@ 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;
|
||||
$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 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.
|
||||
// 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')) {
|
||||
$ht_or_tva_changed = $node->isNew();
|
||||
if (!$ht_or_tva_changed && isset($node->original)) {
|
||||
$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;
|
||||
$ht_or_tva_changed = $orig_ht !== $montant_ht || abs($orig_tva - $tva) > 0.00005;
|
||||
$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 ($ht_or_tva_changed) {
|
||||
$node->set('field_montant_ttc', round($montant_ht * (1 + $tva / 100), 2));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +476,7 @@ function figli_compta_ledger_page_attachments(array &$attachments) {
|
||||
$front_end_routes = [
|
||||
'figli_compta_ledger.home',
|
||||
'figli_compta_ledger.dashboard',
|
||||
'figli_compta_ledger.dashboard_compte',
|
||||
'figli_compta_ledger.history',
|
||||
'figli_compta_ledger.link_entree',
|
||||
];
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
// stores a plain rate (10), which style: 'percent' would otherwise
|
||||
// silently read as 1000%.
|
||||
const PCT = new Intl.NumberFormat('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||
// Mirrors the $taux_officiels select in figli_compta_ledger_form_alter()
|
||||
// and the $official list in figli_compta_ledger_update_8007()/_8008() --
|
||||
// same 0.05-point tolerance as those migration scripts, for the same
|
||||
// "rounding through 2-decimal HT/TTC storage" reason.
|
||||
const OFFICIAL_TVA_RATES = [0, 2.1, 5.5, 10, 20];
|
||||
const OFFICIAL_TVA_EPSILON = 0.05;
|
||||
const MONTHS = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
const TYPE_LABELS = {
|
||||
entree: 'Entrée client',
|
||||
@@ -369,6 +375,7 @@
|
||||
facture: attrs.field_numero_facture || null,
|
||||
libelle: attrs.field_notes || attrs.title,
|
||||
montant_ht: montantHt,
|
||||
cotisation: attrs.field_cotisation_urssaf !== null && attrs.field_cotisation_urssaf !== undefined ? parseFloat(attrs.field_cotisation_urssaf) : null,
|
||||
tva: attrs.field_tva !== null && attrs.field_tva !== undefined ? parseFloat(attrs.field_tva) : null,
|
||||
montant_ttc: attrs.field_montant_ttc !== null && attrs.field_montant_ttc !== undefined ? parseFloat(attrs.field_montant_ttc) : null,
|
||||
parCompte,
|
||||
@@ -680,6 +687,16 @@
|
||||
formatPct(v) {
|
||||
return v === null || v === undefined ? '' : PCT.format(v) + ' %';
|
||||
},
|
||||
// Flags a TVA rate that isn't one of the 5 official French VAT
|
||||
// rates -- either a genuinely blended/multi-rate invoice collapsed
|
||||
// into one ligne_comptable, or a migrated line
|
||||
// figli_compta_ledger_update_8007() left ambiguous rather than
|
||||
// guess (see that function's docblock). Null (no TVA recorded at
|
||||
// all) is never flagged -- nothing to question there.
|
||||
isTvaOfficial(v) {
|
||||
if (v === null || v === undefined) return true;
|
||||
return OFFICIAL_TVA_RATES.some((o) => Math.abs(v - o) < OFFICIAL_TVA_EPSILON);
|
||||
},
|
||||
// Écart between this ouverture row's year and the previous year's
|
||||
// calculated closing balance -- restricted to whichever compte(s)
|
||||
// this specific row's répartition actually touches (ouverture lines
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
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]"]');
|
||||
var type = form.querySelector('[name="field_type_ligne"]');
|
||||
var cotisationActive = form.querySelector('[name="field_cotisation_active[value]"]');
|
||||
var cotisationAmount = form.querySelector('[name="field_cotisation_urssaf[0][value]"]');
|
||||
if (!tva || !ttc) {
|
||||
return;
|
||||
}
|
||||
@@ -42,6 +45,13 @@
|
||||
return parseFloat(tva.value);
|
||||
}
|
||||
|
||||
// Mirrors figli_compta_ledger_node_presave()'s $cotisation_applies:
|
||||
// only "Entrée client" lines with the checkbox on ever go through
|
||||
// the +1,1% step before TVA.
|
||||
function cotisationApplies() {
|
||||
return !!type && type.value === 'entree' && !!cotisationActive && cotisationActive.checked;
|
||||
}
|
||||
|
||||
function recompute() {
|
||||
var htValue = parseFloat(ht.value);
|
||||
if (isNaN(htValue)) {
|
||||
@@ -51,7 +61,22 @@
|
||||
if (isNaN(tvaValue)) {
|
||||
tvaValue = 0;
|
||||
}
|
||||
ttc.value = (htValue * (1 + tvaValue / 100)).toFixed(2);
|
||||
var base = htValue;
|
||||
if (cotisationApplies()) {
|
||||
// Displayed/stored value is just the 1,1% itself (the
|
||||
// delta), not HT + 1,1% -- but TVA still applies to the
|
||||
// augmented base, so $base for the TTC calc below stays
|
||||
// htValue + delta regardless.
|
||||
var delta = Math.round(htValue * 0.011 * 100) / 100;
|
||||
base = htValue + delta;
|
||||
if (cotisationAmount) {
|
||||
cotisationAmount.value = delta.toFixed(2);
|
||||
}
|
||||
}
|
||||
else if (cotisationAmount) {
|
||||
cotisationAmount.value = '';
|
||||
}
|
||||
ttc.value = (base * (1 + tvaValue / 100)).toFixed(2);
|
||||
}
|
||||
|
||||
// Deliberately not called once up front on an existing (edit)
|
||||
@@ -60,17 +85,23 @@
|
||||
// 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).
|
||||
// misrepresent what's actually saved. Only recompute once an
|
||||
// actual input happens, matching the same "only overwrite
|
||||
// Montant TTC when an input 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);
|
||||
}
|
||||
if (type) {
|
||||
type.addEventListener('change', recompute);
|
||||
}
|
||||
if (cotisationActive) {
|
||||
cotisationActive.addEventListener('change', recompute);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,6 +57,34 @@ class HistoryController extends ControllerBase {
|
||||
}
|
||||
|
||||
return [
|
||||
// Same markup/classes as the <nav class="figli-page-nav"> block in
|
||||
// figli-compta-home.html.twig et al. (styled in admin-chrome.css,
|
||||
// pinned next to the page title) -- this controller has no twig
|
||||
// template of its own to put a real <nav> in, but the CSS only
|
||||
// ever targets the class, not the tag, so a render-array
|
||||
// 'container' (<div>) here looks identical. None of the three
|
||||
// links is ever "active" here since this history feed isn't one
|
||||
// of them -- same as visiting it from any of the other pages'
|
||||
// nav, which doesn't include a 4th "Historique" entry either.
|
||||
'nav' => [
|
||||
'#type' => 'container',
|
||||
'#attributes' => ['class' => ['figli-page-nav']],
|
||||
'home' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Grand livre'),
|
||||
'#url' => Url::fromRoute('figli_compta_ledger.home'),
|
||||
],
|
||||
'dashboard' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Dashboard'),
|
||||
'#url' => Url::fromRoute('figli_compta_ledger.dashboard'),
|
||||
],
|
||||
'dashboard_compte' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Par compte'),
|
||||
'#url' => Url::fromRoute('figli_compta_ledger.dashboard_compte'),
|
||||
],
|
||||
],
|
||||
'table' => [
|
||||
'#type' => 'table',
|
||||
'#header' => [
|
||||
|
||||
@@ -43,6 +43,7 @@ class LedgerStatsController extends ControllerBase {
|
||||
|
||||
$par_compte = [];
|
||||
$montant_ht = 0.0;
|
||||
$cotisation = 0.0;
|
||||
$montant_ttc = 0.0;
|
||||
$ecart = 0.0;
|
||||
foreach ($storage->loadMultiple($nids) as $node) {
|
||||
@@ -52,6 +53,13 @@ class LedgerStatsController extends ControllerBase {
|
||||
? (float) $node->get('field_montant_ttc')->value : 0.0;
|
||||
$montant_ht += $ht;
|
||||
$montant_ttc += $ttc;
|
||||
// Blank (never 0) for most lines -- only "Entrée client" lines
|
||||
// with the cotisation checkbox on ever have this field set (see
|
||||
// figli_compta_ledger_node_presave()) -- but summing a blank
|
||||
// value as 0 here is exactly right for a total.
|
||||
if ($node->hasField('field_cotisation_urssaf') && !$node->get('field_cotisation_urssaf')->isEmpty()) {
|
||||
$cotisation += (float) $node->get('field_cotisation_urssaf')->value;
|
||||
}
|
||||
|
||||
$somme = 0.0;
|
||||
foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) {
|
||||
@@ -71,6 +79,7 @@ class LedgerStatsController extends ControllerBase {
|
||||
return new JsonResponse([
|
||||
'annee' => $annee,
|
||||
'montant_ht' => round($montant_ht, 2),
|
||||
'cotisation' => round($cotisation, 2),
|
||||
'montant_ttc' => round($montant_ttc, 2),
|
||||
'ecart' => round($ecart, 2),
|
||||
'par_compte' => array_map(fn ($v) => round($v, 2), $par_compte),
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
<th>Libellé / Détail</th>
|
||||
<th>Signalement</th>
|
||||
<th class="amount figli-ht-col">HT</th>
|
||||
<th class="amount figli-cotisation-col">1,1%</th>
|
||||
<th class="amount figli-tva-col">TVA</th>
|
||||
<th class="amount figli-ttc-col">TTC</th>
|
||||
<th v-for="c in allComptes" :key="c" class="amount compte-col">{{ c }}</th>
|
||||
@@ -137,13 +138,13 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ref="topSentinel" class="figli-sentinel-row">
|
||||
<td :colspan="7 + allComptes.length + 4">
|
||||
<td :colspan="7 + allComptes.length + 5">
|
||||
<span v-if="loadingOlder">Chargement des mois précédents…</span>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="item in groupedRows" :key="item.key">
|
||||
<tr v-if="item.isGroup" class="figli-group-row" :data-year="item.year">
|
||||
<td :colspan="7 + allComptes.length + 4">{{ item.label }} <span class="figli-note">({{ item.count }} lignes)</span></td>
|
||||
<td :colspan="7 + allComptes.length + 5">{{ item.label }} <span class="figli-note">({{ item.count }} lignes)</span></td>
|
||||
</tr>
|
||||
<tr v-else :class="{'figli-flag-row': item.hasFlag}" :data-year="item.date ? item.date.slice(0, 4) : null">
|
||||
<td class="actions-col">
|
||||
@@ -241,14 +242,15 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="amount figli-ht-col" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
|
||||
<td class="amount figli-tva-col">{{ formatPct(item.tva) }}</td>
|
||||
<td class="amount figli-cotisation-col">{{ formatEur(item.cotisation) }}</td>
|
||||
<td class="amount figli-tva-col" :class="{'figli-tva-nonofficial': !isTvaOfficial(item.tva)}" :title="!isTvaOfficial(item.tva) ? 'Taux TVA non officiel (ni 0, 2,1, 5,5, 10 ni 20 %)' : null">{{ formatPct(item.tva) }}</td>
|
||||
<td class="amount figli-ttc-col">{{ formatEur(item.montant_ttc) }}</td>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col">{{ item.parCompte[c] !== undefined ? formatEur(item.parCompte[c]) : '' }}</td>
|
||||
<td class="amount" :class="{'figli-ecart': item.hasError}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
|
||||
<td class="amount" :class="{'figli-ecart': item.hasError && item.ecart < 0, 'figli-ecart-positif': item.hasError && item.ecart > 0}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr ref="bottomSentinel" class="figli-sentinel-row">
|
||||
<td :colspan="7 + allComptes.length + 4">
|
||||
<td :colspan="7 + allComptes.length + 5">
|
||||
<span v-if="loadingNewer">Chargement des mois suivants…</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -261,6 +263,7 @@
|
||||
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
|
||||
</td>
|
||||
<td class="amount figli-ht-col">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }}</td>
|
||||
<td class="amount figli-cotisation-col">{{ currentYearTotals ? formatEur(currentYearTotals.cotisation) : '' }}</td>
|
||||
<td class="amount figli-tva-col"></td>
|
||||
<td class="amount figli-ttc-col">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ttc) : '' }}</td>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="currentYearTotals ? soldeClass(currentYearTotals.par_compte[c]) : ''">{{ currentYearTotals && currentYearTotals.par_compte[c] !== undefined ? formatEur(currentYearTotals.par_compte[c]) : '' }}</td>
|
||||
@@ -321,7 +324,7 @@
|
||||
<td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
|
||||
<td class="amount">{{ formatEur(item.montant_ttc) }}</td>
|
||||
<td v-for="c in modalComptes" :key="c" class="amount compte-col">{{ item.parCompte[c] !== undefined ? formatEur(item.parCompte[c]) : '' }}</td>
|
||||
<td class="amount" :class="{'figli-ecart': item.hasError}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
|
||||
<td class="amount" :class="{'figli-ecart': item.hasError && item.ecart < 0, 'figli-ecart-positif': item.hasError && item.ecart > 0}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
||||
Reference in New Issue
Block a user