Cotisation diffuseur URSSAF (1,1%) : nouveau champ, TTC calculé en cascade
Architecture métier corrigée : la SAS devise HT, ajoute (quand
applicable -- pas systématique) 1,1% de cotisation diffuseur URSSAF
pour l'usage de freelances, puis applique la TVA sur ce montant
augmenté -- pas sur le HT brut. La répartition entre comptes reste
basée sur le HT seul, inchangée.
- field_cotisation_active (case à cocher, "Entrée client" uniquement,
cochée par défaut sur une nouvelle ligne -- optionnelle puisque tous
les devis ne l'incluent pas historiquement).
- field_cotisation_urssaf ("1,1%", montant calculé HT × 1,011, jamais
éditable).
- Montant TTC = round(cotisation × (1 + TVA/100), 2) quand la
cotisation s'applique, round(HT × (1 + TVA/100), 2) sinon --
inchangé pour tous les types hors "Entrée client".
Migration (update hooks 8006/8007) : toutes les lignes "Entrée client"
de field_tva rétro-calculées par figli_compta_ledger_update_8005()
étaient fausses dès que la cotisation s'appliquait (taux mixte HT->TTC,
pas le vrai taux de TVA) -- corrigées avec la même prudence que la
migration précédente : 2021 laissée de côté (pratique non confirmée
sur cette année), et par ligne, un taux déjà "propre" (0/2,1/5,5/10/20
%) signifie qu'aucune cotisation n'a été appliquée (laissé tel quel) ;
sinon le nouveau taux recalculé n'est retenu que s'il retombe lui-même
sur un taux officiel, sinon la ligne reste inchangée plutôt que de
deviner. Montant TTC historique jamais réécrit, comme pour toute
migration de ce module. Résultat : 154 lignes corrigées (cotisation
active), 90 déjà correctes (cotisation inactive), 40 ambiguës laissées
telles quelles, 43 lignes 2021 ignorées.
Formulaire réorganisé en grille à 4 colonnes (HT | 1,1% | TVA | TTC),
case à cocher masquée hors "Entrée client" via #states réel (fiable
ici, contrairement au select TVA -- watch sur field_type_ligne, un
vrai champ, pas un élément ajouté à la main). Colonne "1,1%" ajoutée
au grand livre entre HT et TVA.
This commit is contained in:
@@ -457,6 +457,13 @@ 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%;
|
||||
}
|
||||
|
||||
@@ -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,175 @@ 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.";
|
||||
}
|
||||
|
||||
@@ -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 (Montant HT + 1,1%) si la cotisation ci-dessus est cochée.');
|
||||
}
|
||||
|
||||
// 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,57 @@ 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) {
|
||||
$base = round($montant_ht * 1.011, 2);
|
||||
if ($node->hasField('field_cotisation_urssaf')) {
|
||||
$node->set('field_cotisation_urssaf', $base);
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,6 +369,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,
|
||||
|
||||
@@ -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,17 @@
|
||||
if (isNaN(tvaValue)) {
|
||||
tvaValue = 0;
|
||||
}
|
||||
ttc.value = (htValue * (1 + tvaValue / 100)).toFixed(2);
|
||||
var base = htValue;
|
||||
if (cotisationApplies()) {
|
||||
base = Math.round(htValue * 1.011 * 100) / 100;
|
||||
if (cotisationAmount) {
|
||||
cotisationAmount.value = base.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 +80,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,6 +242,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="amount figli-ht-col" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
|
||||
<td class="amount figli-cotisation-col">{{ formatEur(item.cotisation) }}</td>
|
||||
<td class="amount figli-tva-col">{{ 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>
|
||||
@@ -248,7 +250,7 @@
|
||||
</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"></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>
|
||||
|
||||
Reference in New Issue
Block a user