Files
jekkosandobjecttothis 625bb3536a fix(taxes): reject negative tax rates and percentages (#4730)
* fix(taxes): reject negative tax rates and percentages

Reject negative tax values across all tax entry points so they can
never be persisted and produce inverted sales totals:

- Items::postSave / postBulkUpdate: validate tax_percents.* as
  nonNegativeDecimal (was unvalidated, persisted raw).
- Items::save_tax_data (CSV import): fail the row on a negative
  Tax N Percent instead of silently dropping the tax.
- Taxes::postSave: reject negative tax_rate before saving.
- Config::postSaveTax: reject negative default tax rates.

Adds non-negative regression tests for Items, Taxes and Config, plus
boundary tests proving a zero tax rate/percent is still accepted. New
language strings for the rejection messages.

* test: adopt test{Method}_{Purpose} naming convention for new tests

Align the negative-tax test method names with the convention established
in ConfigTest (testPostSave_RejectsNegativeTaxPercent, etc.), per review.

* fix(config): add language entries to all locales, extract TaxFixtureTrait

* fix(i18n): place tax_rate_non_negative in alphabetical order

Move the new tax_rate_non_negative entry to its correct alphabetical
position (after tax_rate_error_adding_updating) in all locale files.

---------

Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-10-09 08:58:00 +02:00

126 lines
8.8 KiB
PHP

<?php
return [
'add_minus' => 'Inventaire à ajouter ou soustraire.',
'allow_alt_description' => 'Autoriser description alternative',
'amount_entry' => 'Entrée de quantité',
'bulk_edit' => 'Éditions multiples',
'buy_price_required' => 'Le prix d\'achat est requis.',
'cannot_be_deleted' => 'Impossible de supprimer car un ou plusiers des articles sélectionnés ont des ventes.',
'cannot_find_item' => 'Article non trouvé.',
'categories' => '',
'category' => 'Catégorie',
'category_new' => '',
'category_required' => 'La catégorie est requise.',
'change_all_to_allow_alt_desc' => 'Autoriser description alternative pour tous.',
'change_all_to_not_allow_allow_desc' => 'Interdire description alternative pour tous.',
'change_all_to_serialized' => 'Tout sérialiser',
'change_all_to_unserialized' => 'Tout désérialiser',
'change_image' => 'Changer l\'image',
'confirm_bulk_edit' => 'Êtes-vous certain de vouloir éditer toute la sélection ?',
'confirm_bulk_edit_wipe_taxes' => 'Les informations de taxes seront remplacées pour tous les articles.',
'confirm_delete' => 'Êtes-vous certain de vouloir supprimer la sélection ?',
'confirm_restore' => 'Êtes-vous certain de vouloir restaurer le(s) élément(s) sélectionné(s) ?',
'cost_price' => 'Prix de gros',
'cost_price_number' => 'Le prix d\'achat doit être un nombre.',
'cost_price_required' => 'Le prix de gros est requis.',
'count' => 'Mise à jour de l\'inventaire',
'csv_import_failed' => 'Échec d\'import CSV',
'csv_import_invalid_location' => 'Emplacement(s) de stock invalide(s) trouvé(s) : {0}. Seuls les emplacements de stock valides sont autorisés.',
'csv_import_nodata_wrongformat' => 'Le CSV envoyé ne contient aucune donnée, ou elles sont dans un format erroné.',
'csv_import_partially_failed' => 'Il y a eu {0} importation(s) d\'articles échoué(s) au(x) ligne(s) : {1}. Aucune ligne n\'a été importée.',
'csv_import_success' => 'Importation des articles réussie.',
'current_quantity' => 'Quantité actuelle',
'default_pack_name' => 'Chacun',
'description' => 'Description',
'details_count' => 'Détails du décompte d\'inventaire',
'do_nothing' => 'Ne rien faire',
'edit' => '',
'edit_fields_you_want_to_update' => 'Modifier les champs pour les articles sélectionnés.',
'edit_multiple_items' => 'Édition multiple d\'articles',
'empty_upc_items' => 'Articles sans code à barre',
'error_adding_updating' => 'Erreur d\'ajout/édition d\'article',
'error_updating_multiple' => 'Erreur d\'édition d\'articles',
'generate_barcodes' => 'Générer des codes à barres',
'hsn_code' => 'Nomenclature du système harmonisé',
'image' => 'Image',
'import_items_csv' => 'Importation d\'éléments à partir d\'un CSV',
'info_provided_by' => 'Informations fournie par',
'inventory' => 'Inventaire',
'inventory_CSV_import_quantity' => 'Quantité importée à partir d\'un CSV',
'inventory_comments' => 'Commentaires',
'inventory_data_tracking' => 'Suivi d\'inventaire',
'inventory_date' => 'Date',
'inventory_employee' => 'Employé',
'inventory_in_out_quantity' => 'Quantité entrée/sortie',
'inventory_remarks' => 'Remarques',
'is_deleted' => 'Supprimé',
'is_printed' => '',
'is_serialized' => 'L\'article a un numéro de série',
'item' => 'Article',
'item_id' => '',
'item_number' => 'Code à barre',
'item_number_duplicate' => 'L\'article existe déjà la base de données.',
'item_number_invalid' => 'Le code article contient des caractères non autorisés.',
'kit' => 'Ensemble',
'location' => 'Emplacement',
'low_inventory_items' => 'Articles en rupture d\'inventaire',
'low_sell_item' => 'Article à faible vente',
'manually_editing_of_quantity' => 'Édition de quantité manuelle',
'markup' => '',
'name' => 'Nom',
'name_required' => 'Le nom est requis.',
'new' => 'Nouvel article',
'no_description_items' => 'Articles sans description',
'no_items_to_display' => 'Aucun article à afficher.',
'none' => 'Aucun',
'none_selected' => 'Vous n\'avez sélectionné aucun article à modifier',
'nonstock' => 'Non-inventorié',
'number_information' => 'Numéro d\'article',
'number_required' => 'Code à barres est un champ requis.',
'one_or_multiple' => 'article(s)',
'pack_name' => 'Nom de l\'ensemble',
'qty_per_pack' => 'Quantité par ensemble',
'quantity' => 'Quantité',
'quantity_number' => 'La quantité doit être un nombre.',
'quantity_required' => 'La quantité est requise.',
'receiving_quantity' => 'Quantité reçue',
'remove_image' => 'Supprimer l\'image',
'reorder_level' => 'Niveau de réapprovisionnement',
'reorder_level_number' => 'Le niveau de réapprovisionnement doit etre un nombre.',
'reorder_level_required' => 'Le niveau de réapprovisionnement est requis.',
'retrive_item_info' => 'Récupérer les informations d\'article',
'sales_tax_1' => 'Taxe de vente',
'sales_tax_2' => 'Taxe de vente 2',
'search_attributes' => 'Rechercher les attributs sélectionnés',
'select_image' => 'Sélectionner l\'image',
'serialized_items' => 'Articles avec n° de série',
'standard' => 'Régulier',
'stock' => 'Inventaire',
'stock_location' => 'Emplacement de l\'inventaire',
'stock_type' => 'Type d\'inventaire',
'successful_adding' => 'Article ajouté avec succès',
'successful_bulk_edit' => 'Édition d\'article(s) réussie',
'successful_deleted' => 'Suppression réussie',
'successful_updating' => 'Édition d\'article réussie',
'supplier' => 'Fournisseur',
'tax_1' => 'Taxe 1',
'tax_2' => 'Taxe 2',
'tax_3' => '',
'tax_category' => 'Catégorie fiscale',
'tax_name_invalid' => 'Le nom de la taxe contient des caractères non autorisés.',
'tax_percent' => 'Pourcentage d\'imposition',
'tax_percent_number' => 'Le pourcentage d\'imposition doit être une valeur numérique',
'tax_percent_non_negative' => '',
'tax_percent_required' => 'Le pourcentage d\'imposition est requis.',
'tax_percents' => 'Pourcentage de taxes',
'temp' => 'Temporaire',
'type' => 'Type d\'article',
'unit_price' => 'Prix de détail',
'unit_price_number' => 'Le prix unitaire doit etre un nombre.',
'unit_price_required' => 'Le prix de détail est requis.',
'upc_database' => 'Base de données des codes à barres',
'update' => 'Éditer l\'article',
'use_inventory_menu' => 'Utiliser le menu inventaire',
];