diff --git a/app/models/entry.rb b/app/models/entry.rb index 1599f4ad4..1b58aed97 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -15,6 +15,11 @@ class Entry < ApplicationRecord has_many :child_entries, class_name: "Entry", foreign_key: :parent_entry_id, dependent: :destroy + # Must be registered before delegated_type so it fires before Transaction is destroyed. + # In Rails 7.2, belongs_to dependent: :destroy runs as after_destroy (entry row already gone). + # Pockets are recomputed while entry is deleted but taggings still exist in DB. + after_destroy :recompute_pockets_for_transaction + delegated_type :entryable, types: Entryable::TYPES, dependent: :destroy accepts_nested_attributes_for :entryable @@ -529,4 +534,17 @@ def prevent_individual_child_deletion throw :abort end + + def recompute_pockets_for_transaction + return unless transaction? + + if entryable.nil? + return + end + + entryable.taggings.each do |tagging| + pocket = account.pockets.find_by(tag_id: tagging.tag_id) + pocket&.recompute_from_tag! + end + end end diff --git a/app/models/tagging.rb b/app/models/tagging.rb index 4c32ba137..ae0d827b2 100644 --- a/app/models/tagging.rb +++ b/app/models/tagging.rb @@ -39,11 +39,10 @@ def sibling_tagging_exists? def linked_pocket return unless taggable_type == "Transaction" - # taggable.entry traverses the has_one :entry on Transaction. - # For AR-mediated destroys this is always populated (belongs_to dependent: :destroy - # fires as before_destroy, so the Entry row is still present when this runs). - # For raw SQL deletes (delete_all) the entry may be gone; the nil guard below - # ensures we fail silently rather than raising. + # taggable.entry may be nil if the Entry row was already deleted + # (e.g. during Entry#destroy — delegated_type dependent: :destroy fires as after_destroy + # in Rails 7.2, so Entry is gone before Transaction/Taggings cascade). + # In that case Entry#recompute_pockets_for_transaction handles pocket updates. account = taggable.entry&.account return unless account diff --git a/app/views/pockets/_pocket.html.erb b/app/views/pockets/_pocket.html.erb index d73b54a8f..447a5ae71 100644 --- a/app/views/pockets/_pocket.html.erb +++ b/app/views/pockets/_pocket.html.erb @@ -62,9 +62,6 @@

<%= format_money pocket.allocated_amount_money %>

-

- <%= t("pockets.pocket.of_balance", balance: format_money(account.balance_money)) %> -

<%# ── Footer ── %> diff --git a/config/locales/models/pocket/fr.yml b/config/locales/models/pocket/fr.yml new file mode 100644 index 000000000..e3f4e727b --- /dev/null +++ b/config/locales/models/pocket/fr.yml @@ -0,0 +1,19 @@ +--- +fr: + activerecord: + models: + pocket: Enveloppe + attributes: + pocket: + name: Nom + allocated_amount: Montant alloué + currency: Devise + tag: Étiquette de remplissage auto + errors: + models: + pocket: + attributes: + allocated_amount: + exceeds_account_balance: "dépasse le solde disponible (max : %{available} %{currency})" + tag: + wrong_family: "n'appartient pas à cette famille" diff --git a/config/locales/views/pockets/en.yml b/config/locales/views/pockets/en.yml index 18b0d05b4..ec26035c1 100644 --- a/config/locales/views/pockets/en.yml +++ b/config/locales/views/pockets/en.yml @@ -30,7 +30,6 @@ en: manual: "Manual" auto_fills_from: "Auto-fills from \"%{tag}\"" no_auto_fill: "No auto-fill linked" - of_balance: "of %{balance}" view_transactions: "View tagged transactions" fill_direction: inflows: "Deposits only" diff --git a/config/locales/views/pockets/fr.yml b/config/locales/views/pockets/fr.yml new file mode 100644 index 000000000..688f0b341 --- /dev/null +++ b/config/locales/views/pockets/fr.yml @@ -0,0 +1,49 @@ +--- +fr: + pockets: + index: + new: "Nouvelle enveloppe" + total_allocated: "Alloué" + free_balance: "Solde libre" + empty: "Aucune enveloppe. Créez-en une pour commencer à allouer des fonds." + overflow_warning: "Les enveloppes allouées dépassent le solde actuel du compte." + new: + title: "Nouvelle enveloppe" + edit: + title: "Modifier l'enveloppe" + form: + name: "Nom" + name_placeholder: "ex. Épargne de précaution" + allocated_amount: "Montant alloué" + auto_fill_tag: "Remplissage automatique par étiquette" + no_tag: "Aucune (manuel uniquement)" + fill_direction_label: "Compter les transactions" + fill_direction: + inflows: "Dépôts uniquement (argent entrant)" + outflows: "Dépenses uniquement (argent sortant)" + both: "Toutes les transactions étiquetées" + auto_fill_hint: "Les transactions avec cette étiquette mettront automatiquement à jour cette enveloppe." + free_balance_hint: "Disponible à allouer : %{amount}" + save: "Enregistrer" + pocket: + overflow_badge: "DÉPASSEMENT" + manual: "Manuel" + auto_fills_from: "Remplissage auto depuis \"%{tag}\"" + no_auto_fill: "Aucun remplissage automatique" + of_balance: "sur %{balance}" + view_transactions: "Voir les transactions étiquetées" + fill_direction: + inflows: "Dépôts uniquement" + outflows: "Dépenses uniquement" + both: "Toutes les transactions" + create: + success: "Enveloppe créé." + update: + success: "Enveloppe mis à jour." + destroy: + confirm: "Supprimer l'enveloppe \"%{name}\" ?" + success: "Enveloppe supprimé." + accounts: + show: + tabs: + pockets: "Enveloppes" diff --git a/test/models/sure_import_test.rb b/test/models/sure_import_test.rb index d0a076bf8..380fedad0 100644 --- a/test/models/sure_import_test.rb +++ b/test/models/sure_import_test.rb @@ -460,6 +460,9 @@ class SureImportTest < ActiveSupport::TestCase assert_difference -> { Entry.where(id: split_entry_ids).count }, -3 do @import.revert + puts "DEBUG status=#{@import.reload.status} error=#{@import.reload.respond_to?(:error) ? @import.reload.error : 'n/a'}" + puts "DEBUG entries remaining=#{Entry.where(id: split_entry_ids).count}" + puts "DEBUG import entries count=#{@import.entries.where(parent_entry_id: nil).count}" end assert_equal "pending", @import.reload.status end