diff --git a/README.md b/README.md index ae12534b..0b49f035 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ addon | version | maintainers | summary [spreadsheet_dashboard_oca](spreadsheet_dashboard_oca/) | 18.0.1.1.0 | | Use OCA Spreadsheets on dashboards configuration [spreadsheet_dashboard_purchase_oca](spreadsheet_dashboard_purchase_oca/) | 18.0.1.0.0 | | Spreadsheet dashboard for vendors [spreadsheet_dashboard_purchase_stock_oca](spreadsheet_dashboard_purchase_stock_oca/) | 18.0.1.0.0 | | Spreadsheet dashboard for purchases -[spreadsheet_oca](spreadsheet_oca/) | 18.0.1.3.0 | | Allow to edit spreadsheets +[spreadsheet_oca](spreadsheet_oca/) | 18.0.1.3.1 | | Allow to edit spreadsheets +[spreadsheet_quotation](spreadsheet_quotation/) | 18.0.1.0.0 | | Use spreadsheets as quotation calculators linked to sale order templates [//]: # (end addons) diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 41755afb..d90a943c 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,11 +1,12 @@ [project] name = "odoo-addons-oca-spreadsheet" -version = "18.0.20251009.1" +version = "18.0.20260620.0" dependencies = [ "odoo-addon-spreadsheet_dashboard_oca==18.0.*", "odoo-addon-spreadsheet_dashboard_purchase_oca==18.0.*", "odoo-addon-spreadsheet_dashboard_purchase_stock_oca==18.0.*", "odoo-addon-spreadsheet_oca==18.0.*", + "odoo-addon-spreadsheet_quotation==18.0.*", ] classifiers=[ "Programming Language :: Python", diff --git a/spreadsheet_dashboard_oca/i18n/es.po b/spreadsheet_dashboard_oca/i18n/es.po index 3f2ca4ae..a60f21be 100644 --- a/spreadsheet_dashboard_oca/i18n/es.po +++ b/spreadsheet_dashboard_oca/i18n/es.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-07-19 10:02+0000\n" -"PO-Revision-Date: 2026-04-10 13:13+0000\n" +"PO-Revision-Date: 2026-04-17 16:45+0000\n" "Last-Translator: alexmorel-tecnativa \n" "Language-Team: \n" "Language: es\n" diff --git a/spreadsheet_dashboard_oca/i18n/fr.po b/spreadsheet_dashboard_oca/i18n/fr.po index df9cb4ad..778811e6 100644 --- a/spreadsheet_dashboard_oca/i18n/fr.po +++ b/spreadsheet_dashboard_oca/i18n/fr.po @@ -6,163 +6,165 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-06-10 08:46+0000\n" +"Last-Translator: Yann Papouin \n" "Language-Team: none\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__active msgid "Active" -msgstr "" +msgstr "Actif" #. module: spreadsheet_dashboard_oca #. odoo-javascript #: code:addons/spreadsheet_dashboard_oca/static/src/bundle/spreadsheet_to_dashboard.esm.js:0 msgid "Add to dashboard" -msgstr "" +msgstr "Ajouter au tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__can_edit msgid "Can Edit" -msgstr "" +msgstr "Peut éditer" #. module: spreadsheet_dashboard_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_dashboard_oca.spreadsheet_to_dashboard_view_form msgid "Cancel" -msgstr "" +msgstr "Annuler" #. module: spreadsheet_dashboard_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_dashboard_oca.spreadsheet_dashboard_tree_view msgid "Copy" -msgstr "" +msgstr "Copier" #. module: spreadsheet_dashboard_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_dashboard_oca.spreadsheet_to_dashboard_view_form msgid "Create" -msgstr "" +msgstr "Créer" #. module: spreadsheet_dashboard_oca #: model:ir.model,name:spreadsheet_dashboard_oca.model_spreadsheet_to_dashboard msgid "Create dashboard from spreadsheet" -msgstr "" +msgstr "Créer un tableau de bord à partir d'un tableur" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__create_uid msgid "Created by" -msgstr "" +msgstr "Créé par" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__create_date msgid "Created on" -msgstr "" +msgstr "Créé le" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_spreadsheet_import__dashboard_id msgid "Dashboard" -msgstr "" +msgstr "Tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_spreadsheet_import__dashboard_group_id msgid "Dashboard Group" -msgstr "" +msgstr "Groupe Tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__name msgid "Dashboard Name" -msgstr "" +msgstr "Nom du tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__dashboard_group_id msgid "Dashboard Section" -msgstr "" +msgstr "Section Tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__display_name msgid "Display Name" -msgstr "" +msgstr "Nom affiché" #. module: spreadsheet_dashboard_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_dashboard_oca.spreadsheet_dashboard_tree_view msgid "Edit" -msgstr "" +msgstr "Editer" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__id msgid "ID" -msgstr "" +msgstr "ID" #. module: spreadsheet_dashboard_oca #: model:ir.model,name:spreadsheet_dashboard_oca.model_spreadsheet_spreadsheet_import msgid "Import data to spreadsheet" -msgstr "" +msgstr "Importer des données dans un tableur" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Modifié par" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__write_date msgid "Last Updated on" -msgstr "" +msgstr "Modifié le" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__name msgid "Name" -msgstr "" +msgstr "Nom" #. module: spreadsheet_dashboard_oca #: model:spreadsheet.spreadsheet.import.mode,name:spreadsheet_dashboard_oca.spreadsheet_import_mode_dashboard_new_spreadsheet msgid "New dashboard" -msgstr "" +msgstr "Nouveau tableau de bord" #. module: spreadsheet_dashboard_oca #. odoo-python #: code:addons/spreadsheet_dashboard_oca/models/spreadsheet_dashboard.py:0 msgid "Search operation not supported" -msgstr "" +msgstr "Opération de recherche non prise en charge" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__spreadsheet_id msgid "Spreadsheet" -msgstr "" +msgstr "Tableur" #. module: spreadsheet_dashboard_oca #: model:ir.model,name:spreadsheet_dashboard_oca.model_spreadsheet_dashboard msgid "Spreadsheet Dashboard" -msgstr "" +msgstr "Tableau de bord du tableur" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__spreadsheet_raw msgid "Spreadsheet Raw" -msgstr "" +msgstr "Tableur brute" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__spreadsheet_revision_ids msgid "Spreadsheet Revision" -msgstr "" +msgstr "Version du tableur" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_dashboard__spreadsheet_binary_data msgid "Spreadsheet file" -msgstr "" +msgstr "Fichier tableur" #. module: spreadsheet_dashboard_oca #. odoo-python #: code:addons/spreadsheet_dashboard_oca/models/spreadsheet_dashboard.py:0 msgid "The value has to be a boolean" -msgstr "" +msgstr "La valeur doit être un booléen" #. module: spreadsheet_dashboard_oca #: model:spreadsheet.spreadsheet.import.mode,name:spreadsheet_dashboard_oca.spreadsheet_import_mode_dashboard msgid "To dashboard" -msgstr "" +msgstr "Vers le tableau de bord" #. module: spreadsheet_dashboard_oca #: model:ir.model.fields,field_description:spreadsheet_dashboard_oca.field_spreadsheet_to_dashboard__group_ids msgid "User Groups" -msgstr "" +msgstr "Groupes d'utilisateurs" diff --git a/spreadsheet_dashboard_oca/views/spreadsheet_dashboard.xml b/spreadsheet_dashboard_oca/views/spreadsheet_dashboard.xml index fb368fbe..8670d851 100644 --- a/spreadsheet_dashboard_oca/views/spreadsheet_dashboard.xml +++ b/spreadsheet_dashboard_oca/views/spreadsheet_dashboard.xml @@ -17,7 +17,8 @@ not active - + + \n" "Language-Team: French (https://app.transifex.com/odoo/teams/41243/fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % " -"1000000 == 0 ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % " +"1000000 == 0) ? 1 : 2);\n" +"X-Generator: Weblate 5.15.2\n" #. module: spreadsheet_dashboard_purchase_oca #. odoo-javascript #: code:addons/spreadsheet_dashboard_purchase_oca/data/files/vendors_dashboard.json:0 msgid " days" -msgstr "jours" +msgstr " jours" #. module: spreadsheet_dashboard_purchase_oca #. odoo-javascript diff --git a/spreadsheet_dashboard_purchase_stock_oca/i18n/fr.po b/spreadsheet_dashboard_purchase_stock_oca/i18n/fr.po index 9309a8cd..59828706 100644 --- a/spreadsheet_dashboard_purchase_stock_oca/i18n/fr.po +++ b/spreadsheet_dashboard_purchase_stock_oca/i18n/fr.po @@ -10,21 +10,22 @@ msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-26 21:56+0000\n" -"PO-Revision-Date: 2023-10-26 23:09+0000\n" -"Last-Translator: Wil Odoo, 2023\n" +"PO-Revision-Date: 2026-06-10 08:46+0000\n" +"Last-Translator: Yann Papouin \n" "Language-Team: French (https://app.transifex.com/odoo/teams/41243/fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % " -"1000000 == 0 ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % " +"1000000 == 0) ? 1 : 2);\n" +"X-Generator: Weblate 5.15.2\n" #. module: spreadsheet_dashboard_purchase_stock_oca #. odoo-javascript #: code:addons/spreadsheet_dashboard_purchase_stock_oca/data/files/purchase_dashboard.json:0 msgid " days" -msgstr "jours" +msgstr " jours" #. module: spreadsheet_dashboard_purchase_stock_oca #. odoo-javascript @@ -215,13 +216,13 @@ msgstr "Meilleures commandes" #. odoo-javascript #: code:addons/spreadsheet_dashboard_purchase_stock_oca/data/files/purchase_dashboard.json:0 msgid "Top Product Categories" -msgstr "Meilleures catégories de produits" +msgstr "Meilleures catégories d'articles" #. module: spreadsheet_dashboard_purchase_stock_oca #. odoo-javascript #: code:addons/spreadsheet_dashboard_purchase_stock_oca/data/files/purchase_dashboard.json:0 msgid "Top Products" -msgstr "Meilleurs produits" +msgstr "Meilleurs articles" #. module: spreadsheet_dashboard_purchase_stock_oca #. odoo-javascript diff --git a/spreadsheet_dashboard_purchase_stock_oca/i18n/sl.po b/spreadsheet_dashboard_purchase_stock_oca/i18n/sl.po index 69fa23a9..00969092 100644 --- a/spreadsheet_dashboard_purchase_stock_oca/i18n/sl.po +++ b/spreadsheet_dashboard_purchase_stock_oca/i18n/sl.po @@ -17,7 +17,7 @@ msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-26 21:56+0000\n" -"PO-Revision-Date: 2026-02-01 10:10+0000\n" +"PO-Revision-Date: 2026-06-26 10:46+0000\n" "Last-Translator: Matjaz Mozetic \n" "Language-Team: Slovenian (https://app.transifex.com/odoo/teams/41243/sl/)\n" "Language: sl\n" @@ -193,7 +193,7 @@ msgstr "Povpraševanje po ponudbi" #. odoo-javascript #: code:addons/spreadsheet_dashboard_purchase_stock_oca/data/files/purchase_dashboard.json:0 msgid "Responsible" -msgstr "Odgovoren" +msgstr "Odgovorni" #. module: spreadsheet_dashboard_purchase_stock_oca #. odoo-javascript diff --git a/spreadsheet_oca/README.rst b/spreadsheet_oca/README.rst index 3f2808b9..20fb5ca4 100644 --- a/spreadsheet_oca/README.rst +++ b/spreadsheet_oca/README.rst @@ -11,7 +11,7 @@ Spreadsheet Oca !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:afa8556b48ef425ac88f6fa7b69a9f5d76aaef94958f9ce0e14e0efd97024dc0 + !! source digest: sha256:1310916cafc940d7dcb638553bb2072e73f47cc887916d5c384b1cec841b4223 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/spreadsheet_oca/__manifest__.py b/spreadsheet_oca/__manifest__.py index 782aaeb7..4b6779cc 100644 --- a/spreadsheet_oca/__manifest__.py +++ b/spreadsheet_oca/__manifest__.py @@ -5,7 +5,7 @@ "name": "Spreadsheet Oca", "summary": """ Allow to edit spreadsheets""", - "version": "18.0.1.3.0", + "version": "18.0.1.3.1", "license": "AGPL-3", "author": "CreuBlanca,Odoo Community Association (OCA)", "website": "https://github.com/OCA/spreadsheet", diff --git a/spreadsheet_oca/i18n/es.po b/spreadsheet_oca/i18n/es.po index dcf11467..201907f8 100644 --- a/spreadsheet_oca/i18n/es.po +++ b/spreadsheet_oca/i18n/es.po @@ -7,8 +7,8 @@ msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-09-03 11:51+0000\n" -"PO-Revision-Date: 2026-04-13 07:43+0000\n" -"Last-Translator: Ed-Spain \n" +"PO-Revision-Date: 2026-04-17 16:45+0000\n" +"Last-Translator: alexmorel-tecnativa \n" "Language-Team: \n" "Language: es\n" "MIME-Version: 1.0\n" @@ -471,7 +471,7 @@ msgstr "Última actualización el" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Last updated at" -msgstr "Última Actualización el" +msgstr "Última actualización el" #. module: spreadsheet_oca #. odoo-javascript @@ -810,7 +810,7 @@ msgstr "Hoja de cálculo abstracta para herencia" #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__spreadsheet_binary_data #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__spreadsheet_binary_data msgid "Spreadsheet file" -msgstr "Fichero Hoja de Cálculo" +msgstr "Archivo de hoja de cálculo" #. module: spreadsheet_oca #: model:ir.actions.act_window,name:spreadsheet_oca.spreadsheet_spreadsheet_act_window diff --git a/spreadsheet_oca/i18n/fr.po b/spreadsheet_oca/i18n/fr.po index 1fb8857f..e0dd486b 100644 --- a/spreadsheet_oca/i18n/fr.po +++ b/spreadsheet_oca/i18n/fr.po @@ -6,75 +6,75 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-09-17 23:06+0000\n" -"Last-Translator: Grégory Moka Tourisme \n" +"PO-Revision-Date: 2026-06-10 06:35+0000\n" +"Last-Translator: Yann Papouin \n" "Language-Team: none\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.15.2\n" #. module: spreadsheet_oca #: model:ir.model.constraint,message:spreadsheet_oca.constraint_spreadsheet_spreadsheet_tag_name_uniq msgid "A tag with the same name already exists." -msgstr "" +msgstr "Une étiquette portant le même nom existe déjà." #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_needaction msgid "Action Needed" -msgstr "" +msgstr "Nécessite une action" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__active #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__active msgid "Active" -msgstr "" +msgstr "Actif" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_ids msgid "Activities" -msgstr "" +msgstr "Activités" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_exception_decoration msgid "Activity Exception Decoration" -msgstr "" +msgstr "Activité exception décoration" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_state msgid "Activity State" -msgstr "" +msgstr "Statut de l'activité" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_type_icon msgid "Activity Type Icon" -msgstr "" +msgstr "Icône de type d’activité" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Add date" -msgstr "" +msgstr "Ajouter une date" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Add relation" -msgstr "" +msgstr "Ajouter une relation" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Add text" -msgstr "" +msgstr "Ajouter du texte" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/list_controller.xml:0 msgid "Add to spreadesheet" -msgstr "" +msgstr "Ajouter au tableur" #. module: spreadsheet_oca #. odoo-javascript @@ -90,54 +90,54 @@ msgstr "Ajouter au tableur" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "After next" -msgstr "" +msgstr "Après la prochaine" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_kanban_view #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_search_view msgid "Archived" -msgstr "" +msgstr "Archivé" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "Are you sure you want to delete this list?" -msgstr "" +msgstr "Êtes vous sûr de vouloir supprimer cette liste ?" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Area" -msgstr "" +msgstr "Surface" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_attachment_count msgid "Attachment Count" -msgstr "" +msgstr "Nombre de pièces jointes" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Automatically filter on the current period" -msgstr "" +msgstr "Filtrer automatiquement sur la période actuelle" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Automatically filter on the current user" -msgstr "" +msgstr "Filtrer automatiquement sur l'utilisateur actuel" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__badge_image msgid "Badge Background" -msgstr "" +msgstr "Arrière-plan du badge" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "Before Previous" -msgstr "" +msgstr "Avant la précédente" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__can_be_dynamic @@ -160,50 +160,52 @@ msgstr "Modifier le nom" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__client_id msgid "Client" -msgstr "" +msgstr "Client" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__mode #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import_mode__code msgid "Code" -msgstr "" +msgstr "Code" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_tag__color msgid "Color" -msgstr "" +msgstr "Couleur" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Column" -msgstr "" +msgstr "Colonne" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__commands msgid "Commands" -msgstr "" +msgstr "Commandes" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__company_id msgid "Company" -msgstr "" +msgstr "Société" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/pivot_controller.esm.js:0 msgid "Comparisons in pivot are not supported" msgstr "" +"Les comparaisons dans les tableaux croisés dynamiques (pivot) ne sont pas " +"prises en charge" #. module: spreadsheet_oca #: model:ir.ui.menu,name:spreadsheet_oca.spreadsheet_spreadsheet_configuration_menu msgid "Configuration" -msgstr "" +msgstr "Configuration" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view msgid "Context" -msgstr "" +msgstr "Contexte" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__contributor_ids @@ -213,7 +215,7 @@ msgstr "Contributeur" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__contributor_group_ids msgid "Contributors Groups" -msgstr "" +msgstr "Groupes de contributeurs" #. module: spreadsheet_oca #: model:spreadsheet.spreadsheet.import.mode,name:spreadsheet_oca.spreadsheet_import_mode_new @@ -244,25 +246,25 @@ msgstr "Créé le" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet_renderer.esm.js:0 msgid "Currency" -msgstr "" +msgstr "Devise" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__datasource_name msgid "Datasource Name" -msgstr "" +msgstr "Nom de la source de données" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Default value" -msgstr "" +msgstr "Valeur par défaut" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_kanban_view msgid "Delete" -msgstr "" +msgstr "Supprimer" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__display_name @@ -291,6 +293,8 @@ msgstr "Télécharger XLSX" #: code:addons/spreadsheet_oca/static/src/spreadsheet/pivot_controller.esm.js:0 msgid "Duplicated groupbys in pivot are not supported" msgstr "" +"Les regroupements (group_by) en double dans les tableaux croisés dynamiques " +"(pivot) ne sont pas pris en charge" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_select_row_number__dynamic_rows @@ -331,23 +335,23 @@ msgstr "Filtre" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_follower_ids msgid "Followers" -msgstr "" +msgstr "Abonnés" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_partner_ids msgid "Followers (Partners)" -msgstr "" +msgstr "Abonnés (Partenaires)" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_type_icon msgid "Font awesome icon e.g. fa-tasks" -msgstr "" +msgstr "Icône font awesome ex : fa-tasks" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "From / To" -msgstr "" +msgstr "Depuis" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import_mode__group_ids @@ -357,7 +361,7 @@ msgstr "Groupe" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__has_message msgid "Has Message" -msgstr "" +msgstr "A un message" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__id @@ -367,27 +371,27 @@ msgstr "" #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import_mode__id #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_tag__id msgid "ID" -msgstr "" +msgstr "ID" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_exception_icon msgid "Icon" -msgstr "" +msgstr "Icône" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_exception_icon msgid "Icon to indicate an exception activity." -msgstr "" +msgstr "Icône pour indiquer une activité d'exception." #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__message_needaction msgid "If checked, new messages require your attention." -msgstr "" +msgstr "Si coché, de nouveaux messages demandent votre attention." #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__message_has_error msgid "If checked, some messages have a delivery error." -msgstr "" +msgstr "Si actif, certains messages ont une erreur de livraison." #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__company_id @@ -395,6 +399,8 @@ msgid "" "If set, the spreadsheet will be available only if this company is in the " "current companies." msgstr "" +"Si cette option est activée, le tableur ne sera disponible que si cette " +"société figure parmi les sociétés actuelles." #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__import_data @@ -409,7 +415,7 @@ msgstr "Mode d'import" #. module: spreadsheet_oca #: model:ir.actions.act_window,name:spreadsheet_oca.spreadsheet_spreadsheet_import_act_window msgid "Import Pivot" -msgstr "" +msgstr "Importer un tableau croisé dynamique (pivot)" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_spreadsheet_spreadsheet_import @@ -420,23 +426,23 @@ msgstr "Importer des données au tableur" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Include children" -msgstr "" +msgstr "Inclure les fils" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Insert list" -msgstr "" +msgstr "Insérer une liste" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_is_follower msgid "Is Follower" -msgstr "" +msgstr "Est un abonné" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__is_tree msgid "Is Tree" -msgstr "" +msgstr "Est une liste" #. module: spreadsheet_oca #. odoo-javascript @@ -474,19 +480,19 @@ msgstr "Dernière mise à jour à" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Line" -msgstr "" +msgstr "Ligne" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Link to Odoo menu" -msgstr "" +msgstr "Lien vers le menu Odoo" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "List name" -msgstr "" +msgstr "Nom de la liste" #. module: spreadsheet_oca #: model:res.groups,name:spreadsheet_oca.group_manager @@ -498,22 +504,22 @@ msgstr "Gestionnaire" #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/chart_panels.esm.js:0 #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Menu Items" -msgstr "" +msgstr "Éléments de menu" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_has_error msgid "Message Delivery error" -msgstr "" +msgstr "Erreur de livraison du message" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_ids msgid "Messages" -msgstr "" +msgstr "Messages" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__mode_id msgid "Mode" -msgstr "" +msgstr "Mode" #. module: spreadsheet_oca #. odoo-javascript @@ -525,7 +531,7 @@ msgstr "Modèle" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_ir_model msgid "Models" -msgstr "" +msgstr "Modèles" #. module: spreadsheet_oca #. odoo-javascript @@ -537,17 +543,17 @@ msgstr "Mois" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "Month / Quarter" -msgstr "" +msgstr "Mois / Trimestre" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__my_activity_date_deadline msgid "My Activity Deadline" -msgstr "" +msgstr "Mon échéance d'activité" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_search_view msgid "My Sheets" -msgstr "" +msgstr "Mes feuilles" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__name @@ -567,22 +573,22 @@ msgstr "Suivant" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_date_deadline msgid "Next Activity Deadline" -msgstr "" +msgstr "Échéance de la prochaine activité" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_summary msgid "Next Activity Summary" -msgstr "" +msgstr "Résumé de la prochaine activité" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_type_id msgid "Next Activity Type" -msgstr "" +msgstr "Type de la prochaine activité" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__next_revision_id msgid "Next Revision" -msgstr "" +msgstr "Prochaine version" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_select_row_number__number_of_rows @@ -593,22 +599,22 @@ msgstr "Nombre de lignes" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_needaction_counter msgid "Number of Actions" -msgstr "" +msgstr "Nombre d'actions" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__message_has_error_counter msgid "Number of errors" -msgstr "" +msgstr "Nombre d'erreurs" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__message_needaction_counter msgid "Number of messages requiring action" -msgstr "" +msgstr "Nombre de messages nécessitant une action" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__message_has_error_counter msgid "Number of messages with delivery error" -msgstr "" +msgstr "Nombre de messages avec erreur de livraison" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_import_form_view @@ -623,19 +629,19 @@ msgstr "Propriétaire" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view msgid "Ownership" -msgstr "" +msgstr "Propriété" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Pie" -msgstr "" +msgstr "Camembert" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Possible values" -msgstr "" +msgstr "Valeurs possibles" #. module: spreadsheet_oca #. odoo-javascript @@ -647,24 +653,24 @@ msgstr "Précédent" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Quarter" -msgstr "" +msgstr "Trimestre" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "Re-insert Dynamic" -msgstr "" +msgstr "Réinsérer dynamique" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "Re-insert Static" -msgstr "" +msgstr "Réinsérer statique" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view msgid "Read Access" -msgstr "" +msgstr "Accès en lecture" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__reader_ids @@ -674,7 +680,7 @@ msgstr "Lecteurs" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__reader_group_ids msgid "Readers Groups" -msgstr "" +msgstr "Groupes de lecteurs" #. module: spreadsheet_oca #. odoo-javascript @@ -686,13 +692,13 @@ msgstr "Mettre à jour les données" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Related model" -msgstr "" +msgstr "Model associé" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "Relative Period" -msgstr "" +msgstr "Période relative" #. module: spreadsheet_oca #. odoo-javascript @@ -703,24 +709,24 @@ msgstr "Supprimer" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__res_id msgid "Res" -msgstr "" +msgstr "Res" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_user_id msgid "Responsible User" -msgstr "" +msgstr "Responsable" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Restrict values with a domain" -msgstr "" +msgstr "Limiter les valeurs à un domaine" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Rows:" -msgstr "" +msgstr "Lignes :" #. module: spreadsheet_oca #. odoo-javascript @@ -734,12 +740,12 @@ msgstr "Enregistrer" #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/chart_panels.esm.js:0 #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Select a menu..." -msgstr "" +msgstr "Sélectionnez un menu..." #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_spreadsheet_select_row_number msgid "Select number of rows to duplicate row" -msgstr "" +msgstr "Sélectionnez le nombre de lignes à dupliquer" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import_mode__sequence @@ -749,19 +755,19 @@ msgstr "Séquence" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__server_revision_id msgid "Server Revision" -msgstr "" +msgstr "Version du serveur" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter.esm.js:0 msgid "Settings" -msgstr "" +msgstr "Paramètres" #. module: spreadsheet_oca #. odoo-python #: code:addons/spreadsheet_oca/models/spreadsheet_abstract.py:0 msgid "Sheet1" -msgstr "" +msgstr "Feuille1" #. module: spreadsheet_oca #. odoo-javascript @@ -774,81 +780,81 @@ msgstr "Tri" #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet_import__spreadsheet_id #: model:ir.module.category,name:spreadsheet_oca.module_category_spreedsheet msgid "Spreadsheet" -msgstr "Feuille de calcul" +msgstr "Tableur" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_spreadsheet_oca_revision msgid "Spreadsheet Oca Revision" -msgstr "" +msgstr "Version OCA du tableur" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__spreadsheet_raw #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__spreadsheet_raw msgid "Spreadsheet Raw" -msgstr "" +msgstr "Tableur brute" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__spreadsheet_revision_ids #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__spreadsheet_revision_ids msgid "Spreadsheet Revision" -msgstr "" +msgstr "Version du tableur" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_spreadsheet_spreadsheet_tag msgid "Spreadsheet Tag" -msgstr "" +msgstr "Étiquette de tableur" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_spreadsheet_abstract msgid "Spreadsheet abstract for inheritance" -msgstr "" +msgstr "Tableur abstrait pour héritage" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_abstract__spreadsheet_binary_data #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__spreadsheet_binary_data msgid "Spreadsheet file" -msgstr "" +msgstr "Fichier tableur" #. module: spreadsheet_oca #: model:ir.actions.act_window,name:spreadsheet_oca.spreadsheet_spreadsheet_act_window #: model:ir.ui.menu,name:spreadsheet_oca.spreadsheet_spreadsheet_menu msgid "Spreadsheets" -msgstr "Feuilles de calcul" +msgstr "Tableurs" #. module: spreadsheet_oca #: model:ir.actions.act_window,name:spreadsheet_oca.spreadsheet_spreadsheet_tags_act_window #: model:ir.ui.menu,name:spreadsheet_oca.spreadsheet_spreadsheet_tag_menu msgid "Spreadsheets Tags" -msgstr "" +msgstr "Étiquettes de tableurs" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_kanban_view msgid "Spreasheet Image" -msgstr "" +msgstr "Image du tableur" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Stacked" -msgstr "" +msgstr "Empilé" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Stacked Area" -msgstr "" +msgstr "Graphique en aires empilées" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Stacked Column" -msgstr "" +msgstr "Colonne empilée" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/odoo_panels.esm.js:0 msgid "Stacked Line" -msgstr "" +msgstr "Ligne empilée" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_state @@ -858,11 +864,15 @@ msgid "" "Today: Activity date is today\n" "Planned: Future activities." msgstr "" +"Statut basé sur les activités\n" +"En retard : la date d'échéance est déjà dépassée\n" +"Aujourd'hui : la date d'activité est aujourd'hui\n" +"Planifiée : activités futures." #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__spreadsheet_tag_ids msgid "Tags" -msgstr "" +msgstr "Étiquettes" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet_import__dynamic @@ -871,58 +881,58 @@ msgid "" "filters set in the spreadsheets." msgstr "" "Ce champ vous permet de générer des tableaux dont les lignes sont mises à " -"jour avec les filtres définis dans les feuilles de calcul." +"jour avec les filtres définis dans les tableurs." #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Time range" -msgstr "" +msgstr "Intervalle de temps" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Toggle Dropdown" -msgstr "" +msgstr "Permuter le menu déroulant" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "Total rows used when inserting list" -msgstr "" +msgstr "Nombre total de lignes utilisées lors de l'insertion de la liste" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet_tag__color msgid "Transparent tags are not visible in the kanban view" -msgstr "" +msgstr "Les étiquettes transparentes ne sont pas visibles dans la vue Kanban" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_oca_revision__type msgid "Type" -msgstr "" +msgstr "Type" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__activity_exception_decoration msgid "Type of the exception activity on record." -msgstr "" +msgstr "Type d'activité d'exception enregistrée." #. module: spreadsheet_oca #. odoo-python #: code:addons/spreadsheet_oca/models/spreadsheet_spreadsheet.py:0 msgid "Unnamed" -msgstr "" +msgstr "Sans nom" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet_tree/spreadsheet_tree_view.xml:0 msgid "Upload" -msgstr "" +msgstr "Téléverser" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet_tree/spreadsheet_tree_view.esm.js:0 msgid "Upload a Spreadsheet" -msgstr "" +msgstr "Importer une feuille de calcul" #. module: spreadsheet_oca #: model:res.groups,name:spreadsheet_oca.group_user @@ -932,17 +942,17 @@ msgstr "Utilisateur" #. module: spreadsheet_oca #: model:ir.model.fields,field_description:spreadsheet_oca.field_spreadsheet_spreadsheet__website_message_ids msgid "Website Messages" -msgstr "" +msgstr "Messages du site web" #. module: spreadsheet_oca #: model:ir.model.fields,help:spreadsheet_oca.field_spreadsheet_spreadsheet__website_message_ids msgid "Website communication history" -msgstr "" +msgstr "Historique de communication du site web" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view msgid "Write Access" -msgstr "" +msgstr "Accès en écriture" #. module: spreadsheet_oca #. odoo-javascript @@ -954,35 +964,35 @@ msgstr "Année" #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "ascending" -msgstr "Croissant" +msgstr "croissant" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "descending" -msgstr "Décroissant" +msgstr "décroissant" #. module: spreadsheet_oca #: model_terms:ir.ui.view,arch_db:spreadsheet_oca.spreadsheet_spreadsheet_form_view msgid "e.g. Sales KPI" -msgstr "" +msgstr "ex : indicateurs clés de performance (KPI) des ventes" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/spreadsheet.xml:0 msgid "name is required" -msgstr "Le nom est requis" +msgstr "« nom » est requis" #. module: spreadsheet_oca #. odoo-javascript #: code:addons/spreadsheet_oca/static/src/spreadsheet/bundle/filter_panel_datasources.esm.js:0 msgid "not updated" -msgstr "" +msgstr "n'a pas été mis à jour" #. module: spreadsheet_oca #: model:ir.model,name:spreadsheet_oca.model_ir_websocket msgid "websocket message handling" -msgstr "" +msgstr "gestion des messages websocket" #~ msgid "Dimensions" #~ msgstr "Dimensions" diff --git a/spreadsheet_oca/static/description/index.html b/spreadsheet_oca/static/description/index.html index 877f077a..739577db 100644 --- a/spreadsheet_oca/static/description/index.html +++ b/spreadsheet_oca/static/description/index.html @@ -372,7 +372,7 @@

Spreadsheet Oca

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:afa8556b48ef425ac88f6fa7b69a9f5d76aaef94958f9ce0e14e0efd97024dc0 +!! source digest: sha256:1310916cafc940d7dcb638553bb2072e73f47cc887916d5c384b1cec841b4223 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/spreadsheet Translate me on Weblate Try me on Runboat

This module adds a functionality for adding and editing Spreadsheets diff --git a/spreadsheet_oca/static/src/spreadsheet/list_renderer.esm.js b/spreadsheet_oca/static/src/spreadsheet/list_renderer.esm.js index 9ead5d58..9cf5bd54 100644 --- a/spreadsheet_oca/static/src/spreadsheet/list_renderer.esm.js +++ b/spreadsheet_oca/static/src/spreadsheet/list_renderer.esm.js @@ -52,6 +52,7 @@ patch(ListRenderer.prototype, { return this.columns .filter( (col) => + fields[col.name] && col.type === "field" && col.field.component !== HandleField && !col.relatedPropertyField && diff --git a/spreadsheet_oca/views/spreadsheet_spreadsheet.xml b/spreadsheet_oca/views/spreadsheet_spreadsheet.xml index 01a49d1d..b857dc00 100644 --- a/spreadsheet_oca/views/spreadsheet_spreadsheet.xml +++ b/spreadsheet_oca/views/spreadsheet_spreadsheet.xml @@ -53,7 +53,8 @@

- + + - + + diff --git a/spreadsheet_oca/wizards/spreadsheet_spreadsheet_import.xml b/spreadsheet_oca/wizards/spreadsheet_spreadsheet_import.xml index 34656f92..68194017 100644 --- a/spreadsheet_oca/wizards/spreadsheet_spreadsheet_import.xml +++ b/spreadsheet_oca/wizards/spreadsheet_spreadsheet_import.xml @@ -18,8 +18,10 @@ required="mode == 'new'" /> - - + + + + - - + + + + `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Setting up a quotation calculator + +:: + + + 1. Go to **Sales > Configuration > Quotation Templates**. + 2. Open or create a quotation template. + 3. Click **Create Calculator** next to the "Quotation Calculator" field. + 4. A wizard will open; set a name and number of initial rows, then click + **Create Calculator**. + 5. The spreadsheet editor opens with a pre-configured list of + ``sale.order.line`` fields. + 6. Customize the spreadsheet: add formulas, charts, or extra columns as + needed. + 7. Save the spreadsheet. + + Using the calculator on a sale order + +1. Create a new quotation and select the template that has a calculator. +2. A **Calculator** smart button appears on the sale order form. +3. Click it to open the spreadsheet filtered to the current order's + lines. +4. Edit values in the spreadsheet. +5. Use **File > Field Sync** to map columns to sale order line fields. +6. Save the spreadsheet from the editor. + +Syncing values back to the order + +:: + + + After configuring field sync mappings in the spreadsheet, the mapped + column values can be pushed to the corresponding sale order line fields + when saving the spreadsheet or from the sale order form. + + Step-by-step guide + ~~~~~~~~~~~~~~~~~~ + + [1. Add quotation templates in settings](image1.png) + [2. Create a new template & create a calculator](image2.png) + [3. Map Sync fields > columns to fields](image3.png) + [4. Save template > create a quotation from template and open calculator](image4.png) + [5. add new lines to the calculator, save and sync](image5.png) + [6. Add new products if any](image6.png) + [7. Quotation updated!!!](image7.png) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Cloud Lotus + +Contributors +------------ + +- OCA Contributors + +- `Cloud Lotus `__ + + - Cifuentes Roberto + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/spreadsheet `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/spreadsheet_quotation/__init__.py b/spreadsheet_quotation/__init__.py new file mode 100644 index 00000000..cefe6dff --- /dev/null +++ b/spreadsheet_quotation/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizards diff --git a/spreadsheet_quotation/__manifest__.py b/spreadsheet_quotation/__manifest__.py new file mode 100644 index 00000000..7e09205c --- /dev/null +++ b/spreadsheet_quotation/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Spreadsheet Quotation Calculator", + "summary": ( + "Use spreadsheets as quotation calculators linked " "to sale order templates" + ), + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA), Cloud Lotus", + "website": "https://github.com/OCA/spreadsheet", + "depends": ["spreadsheet_oca", "sale_management"], + "data": [ + "security/ir.model.access.csv", + "wizards/spreadsheet_quotation_create.xml", + "views/sale_order_template_views.xml", + "views/sale_order_views.xml", + ], + "assets": { + "spreadsheet.o_spreadsheet": [ + "spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js", + "spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js", + "spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml", + "spreadsheet_quotation/static/src/quotation_spreadsheet/quotation_spreadsheet.xml", + ], + }, + "installable": True, + "development_status": "Alpha", +} diff --git a/spreadsheet_quotation/i18n/it.po b/spreadsheet_quotation/i18n/it.po new file mode 100644 index 00000000..36a10540 --- /dev/null +++ b/spreadsheet_quotation/i18n/it.po @@ -0,0 +1,386 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spreadsheet_quotation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "%s line(s) created" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "%s line(s) updated" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_view_form_inherit +msgid "Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_template_view_form_inherit +msgid "Create Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_template_view_form_inherit +msgid "Open Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Add" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Add Mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +msgid "Calculator - %s" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Cancel" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Cannot sync a cancelled quotation." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Cannot sync a confirmed sales order." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Column" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Column Mappings" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Create Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Create New Products" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Create Quotation Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_spreadsheet_quotation_create +msgid "Create Quotation Calculator Spreadsheet" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Create and Sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__create_uid +msgid "Created by" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__create_date +msgid "Created on" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Description" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Discount (%)" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__display_name +msgid "Display Name" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Do you want to create them?" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Error syncing: %s" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Field" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Field Sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order__has_spreadsheet +msgid "Has Spreadsheet" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__id +msgid "ID" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,help:spreadsheet_quotation.field_spreadsheet_quotation_create__line_count +msgid "Initial number of rows to display in the list." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__write_date +msgid "Last Updated on" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Linked to Sale Order #" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "" +"Map spreadsheet columns to sale order line fields.\n" +" Existing lines will be updated in place; new rows with\n" +" product data will be added as new quotation lines." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__name +msgid "Name" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +msgid "New" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "" +"New rows with a product name will be added as new\n" +" quotation lines. If a product is not found in the\n" +" catalog you will be asked to confirm its creation." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No field mappings configured" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "No mappings configured yet." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No sale order line list found" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "No sale order line list found in this spreadsheet." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No sale order linked. Set the Sale Order filter first." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No values to sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__line_count +msgid "Number of lines" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Product" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Product sync enabled." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Quantity" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order__spreadsheet_id +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order_template__spreadsheet_id +msgid "Quotation Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_sale_order_template +msgid "Quotation Template" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py:0 +msgid "Sale Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py:0 +msgid "Sale Order Lines" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__sale_order_template_id +msgid "Sale Order Template" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Select..." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Set the Sale Order filter to link this calculator to an order." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,help:spreadsheet_quotation.field_sale_order_template__spreadsheet_id +msgid "" +"Spreadsheet used as pricing calculator for quotations created from this " +"template." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Sync cancelled" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Sync to Sale Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Syncing..." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "" +"The following products were not found in the catalog and will be created:" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "This column already has a mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "This field already has a mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Unit Price" +msgstr "" diff --git a/spreadsheet_quotation/i18n/spreadsheet_quotation.pot b/spreadsheet_quotation/i18n/spreadsheet_quotation.pot new file mode 100644 index 00000000..35ae9608 --- /dev/null +++ b/spreadsheet_quotation/i18n/spreadsheet_quotation.pot @@ -0,0 +1,385 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spreadsheet_quotation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "%s line(s) created" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "%s line(s) updated" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_view_form_inherit +msgid "Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_template_view_form_inherit +msgid "Create Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.sale_order_template_view_form_inherit +msgid "Open Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Add" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Add Mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +msgid "Calculator - %s" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Cancel" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Cannot sync a cancelled quotation." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Cannot sync a confirmed sales order." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Column" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Column Mappings" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Create Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Create New Products" +msgstr "" + +#. module: spreadsheet_quotation +#: model_terms:ir.ui.view,arch_db:spreadsheet_quotation.spreadsheet_quotation_create_form +msgid "Create Quotation Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_spreadsheet_quotation_create +msgid "Create Quotation Calculator Spreadsheet" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Create and Sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__create_uid +msgid "Created by" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__create_date +msgid "Created on" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Description" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Discount (%)" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__display_name +msgid "Display Name" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Do you want to create them?" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Error syncing: %s" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Field" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js:0 +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Field Sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order__has_spreadsheet +msgid "Has Spreadsheet" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__id +msgid "ID" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,help:spreadsheet_quotation.field_spreadsheet_quotation_create__line_count +msgid "Initial number of rows to display in the list." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__write_date +msgid "Last Updated on" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Linked to Sale Order #" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "" +"Map spreadsheet columns to sale order line fields.\n" +" Existing lines will be updated in place; new rows with\n" +" product data will be added as new quotation lines." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__name +msgid "Name" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/models/sale_order.py:0 +msgid "New" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "" +"New rows with a product name will be added as new\n" +" quotation lines. If a product is not found in the\n" +" catalog you will be asked to confirm its creation." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No field mappings configured" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "No mappings configured yet." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No sale order line list found" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "No sale order line list found in this spreadsheet." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No sale order linked. Set the Sale Order filter first." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "No values to sync" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__line_count +msgid "Number of lines" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Product" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Product sync enabled." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Quantity" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order__spreadsheet_id +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_sale_order_template__spreadsheet_id +msgid "Quotation Calculator" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_sale_order_template +msgid "Quotation Template" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py:0 +msgid "Sale Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-python +#: code:addons/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py:0 +msgid "Sale Order Lines" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,field_description:spreadsheet_quotation.field_spreadsheet_quotation_create__sale_order_template_id +msgid "Sale Order Template" +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model,name:spreadsheet_quotation.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Select..." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Set the Sale Order filter to link this calculator to an order." +msgstr "" + +#. module: spreadsheet_quotation +#: model:ir.model.fields,help:spreadsheet_quotation.field_sale_order_template__spreadsheet_id +msgid "" +"Spreadsheet used as pricing calculator for quotations created from this " +"template." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Sync cancelled" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Sync to Sale Order" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml:0 +msgid "Syncing..." +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "" +"The following products were not found in the catalog and will be created:" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "This column already has a mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "This field already has a mapping" +msgstr "" + +#. module: spreadsheet_quotation +#. odoo-javascript +#: code:addons/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js:0 +msgid "Unit Price" +msgstr "" diff --git a/spreadsheet_quotation/models/__init__.py b/spreadsheet_quotation/models/__init__.py new file mode 100644 index 00000000..e7c158fb --- /dev/null +++ b/spreadsheet_quotation/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import sale_order_template +from . import sale_order diff --git a/spreadsheet_quotation/models/sale_order.py b/spreadsheet_quotation/models/sale_order.py new file mode 100644 index 00000000..c118bde8 --- /dev/null +++ b/spreadsheet_quotation/models/sale_order.py @@ -0,0 +1,94 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + spreadsheet_id = fields.Many2one( + "spreadsheet.spreadsheet", + string="Quotation Calculator", + copy=False, + ) + has_spreadsheet = fields.Boolean( + compute="_compute_has_spreadsheet", + ) + + @api.depends("spreadsheet_id") + def _compute_has_spreadsheet(self): + for order in self: + order.has_spreadsheet = bool(order.spreadsheet_id) + + @api.onchange("sale_order_template_id") + def _onchange_sale_order_template_id_spreadsheet(self): + if self.sale_order_template_id and self.sale_order_template_id.spreadsheet_id: + spreadsheet = self._copy_template_spreadsheet( + self.sale_order_template_id.spreadsheet_id, + ) + self.spreadsheet_id = spreadsheet + elif not self.sale_order_template_id: + self.spreadsheet_id = False + + def _copy_template_spreadsheet(self, template_spreadsheet): + """Create a copy of the template spreadsheet for this sale order.""" + return template_spreadsheet.copy( + {"name": _("Calculator - %s", self.name or _("New"))} + ) + + def _update_spreadsheet_filter(self): + """Update the global filter default value in the spreadsheet JSON + so the list is filtered to this sale order's id.""" + self.ensure_one() + if not self.spreadsheet_id: + return + data = self.spreadsheet_id.spreadsheet_raw + if not data: + return + if isinstance(data, str): + data = json.loads(data) + + for gf in data.get("globalFilters", []): + if gf.get("type") == "relation" and gf.get("modelName") == "sale.order": + gf["defaultValue"] = [self.id] + break + + self.spreadsheet_id.spreadsheet_raw = data + + def write(self, vals): + res = super().write(vals) + if "spreadsheet_id" in vals: + for order in self.filtered("spreadsheet_id"): + order._update_spreadsheet_filter() + return res + + @api.model_create_multi + def create(self, vals_list): + orders = super().create(vals_list) + for order in orders.filtered("spreadsheet_id"): + order._update_spreadsheet_filter() + return orders + + def action_open_spreadsheet_calculator(self): + self.ensure_one() + if self.spreadsheet_id: + return self.spreadsheet_id.open_spreadsheet() + return False + + def _check_spreadsheet_sync_allowed(self): + self.ensure_one() + if self.state == "cancel": + raise UserError(_("Cannot sync a cancelled quotation.")) + if self.state == "sale": + raise UserError(_("Cannot sync a confirmed sales order.")) + + def action_sync_spreadsheet_order_lines(self, commands): + """Sync sale order lines from the quotation calculator spreadsheet.""" + self.ensure_one() + self._check_spreadsheet_sync_allowed() + self.write({"order_line": commands}) + return True diff --git a/spreadsheet_quotation/models/sale_order_template.py b/spreadsheet_quotation/models/sale_order_template.py new file mode 100644 index 00000000..72a001a8 --- /dev/null +++ b/spreadsheet_quotation/models/sale_order_template.py @@ -0,0 +1,35 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleOrderTemplate(models.Model): + _inherit = "sale.order.template" + + spreadsheet_id = fields.Many2one( + "spreadsheet.spreadsheet", + string="Quotation Calculator", + copy=True, + help="Spreadsheet used as pricing calculator for quotations " + "created from this template.", + ) + + def action_create_spreadsheet_calculator(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": "Create Quotation Calculator", + "res_model": "spreadsheet.quotation.create", + "view_mode": "form", + "target": "new", + "context": { + "default_sale_order_template_id": self.id, + }, + } + + def action_open_spreadsheet_calculator(self): + self.ensure_one() + if self.spreadsheet_id: + return self.spreadsheet_id.open_spreadsheet() + return self.action_create_spreadsheet_calculator() diff --git a/spreadsheet_quotation/pyproject.toml b/spreadsheet_quotation/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/spreadsheet_quotation/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spreadsheet_quotation/readme/CONTRIBUTORS.md b/spreadsheet_quotation/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..65201895 --- /dev/null +++ b/spreadsheet_quotation/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* OCA Contributors +- [Cloud Lotus](https://cloudlotus.net) + - Cifuentes Roberto \ No newline at end of file diff --git a/spreadsheet_quotation/readme/DESCRIPTION.md b/spreadsheet_quotation/readme/DESCRIPTION.md new file mode 100644 index 00000000..c1196843 --- /dev/null +++ b/spreadsheet_quotation/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +This module allows linking spreadsheet calculators to quotation templates +in Odoo. When a sale order is created from a template that has a +calculator, a copy of the spreadsheet is automatically assigned to the +order with a pre-configured global filter so the ODOO.LIST formulas +display only that order's lines. + +The spreadsheet calculator is built on top of ``spreadsheet_oca`` and +uses ODOO.LIST formulas to display sale order line data (product, +quantity, unit price, etc.). Users can add custom formulas, calculations, +and charts to build complex pricing logic. + +A **Field Sync** side panel lets users map spreadsheet columns to +sale order line fields. This column-based approach is more intuitive +than cell-by-cell mapping and makes it easy to push calculated values +back to the sale order. diff --git a/spreadsheet_quotation/readme/USAGE.md b/spreadsheet_quotation/readme/USAGE.md new file mode 100644 index 00000000..a714f3c1 --- /dev/null +++ b/spreadsheet_quotation/readme/USAGE.md @@ -0,0 +1,42 @@ +Setting up a quotation calculator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Go to **Sales > Configuration > Quotation Templates**. +2. Open or create a quotation template. +3. Click **Create Calculator** next to the "Quotation Calculator" field. +4. A wizard will open; set a name and number of initial rows, then click + **Create Calculator**. +5. The spreadsheet editor opens with a pre-configured list of + ``sale.order.line`` fields. +6. Customize the spreadsheet: add formulas, charts, or extra columns as + needed. +7. Save the spreadsheet. + +Using the calculator on a sale order +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Create a new quotation and select the template that has a calculator. +2. A **Calculator** smart button appears on the sale order form. +3. Click it to open the spreadsheet filtered to the current order's + lines. +4. Edit values in the spreadsheet. +5. Use **File > Field Sync** to map columns to sale order line fields. +6. Save the spreadsheet from the editor. + +Syncing values back to the order +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After configuring field sync mappings in the spreadsheet, the mapped +column values can be pushed to the corresponding sale order line fields +when saving the spreadsheet or from the sale order form. + +Step-by-step guide +~~~~~~~~~~~~~~~~~~ + +[1. Add quotation templates in settings](image1.png) +[2. Create a new template & create a calculator](image2.png) +[3. Map Sync fields > columns to fields](image3.png) +[4. Save template > create a quotation from template and open calculator](image4.png) +[5. add new lines to the calculator, save and sync](image5.png) +[6. Add new products if any](image6.png) +[7. Quotation updated!!!](image7.png) \ No newline at end of file diff --git a/spreadsheet_quotation/readme/image1.png b/spreadsheet_quotation/readme/image1.png new file mode 100644 index 00000000..1eff55c7 Binary files /dev/null and b/spreadsheet_quotation/readme/image1.png differ diff --git a/spreadsheet_quotation/readme/image2.png b/spreadsheet_quotation/readme/image2.png new file mode 100644 index 00000000..eda72070 Binary files /dev/null and b/spreadsheet_quotation/readme/image2.png differ diff --git a/spreadsheet_quotation/readme/image3.png b/spreadsheet_quotation/readme/image3.png new file mode 100644 index 00000000..0c6dc222 Binary files /dev/null and b/spreadsheet_quotation/readme/image3.png differ diff --git a/spreadsheet_quotation/readme/image4.png b/spreadsheet_quotation/readme/image4.png new file mode 100644 index 00000000..c03e9d8f Binary files /dev/null and b/spreadsheet_quotation/readme/image4.png differ diff --git a/spreadsheet_quotation/readme/image5.png b/spreadsheet_quotation/readme/image5.png new file mode 100644 index 00000000..d163ba0e Binary files /dev/null and b/spreadsheet_quotation/readme/image5.png differ diff --git a/spreadsheet_quotation/readme/image6.png b/spreadsheet_quotation/readme/image6.png new file mode 100644 index 00000000..15615675 Binary files /dev/null and b/spreadsheet_quotation/readme/image6.png differ diff --git a/spreadsheet_quotation/readme/image7.png b/spreadsheet_quotation/readme/image7.png new file mode 100644 index 00000000..92824d69 Binary files /dev/null and b/spreadsheet_quotation/readme/image7.png differ diff --git a/spreadsheet_quotation/security/ir.model.access.csv b/spreadsheet_quotation/security/ir.model.access.csv new file mode 100644 index 00000000..f4ec051d --- /dev/null +++ b/spreadsheet_quotation/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_spreadsheet_quotation_create,access_spreadsheet_quotation_create,model_spreadsheet_quotation_create,sales_team.group_sale_salesman,1,1,1,1 diff --git a/spreadsheet_quotation/static/description/icon.png b/spreadsheet_quotation/static/description/icon.png new file mode 100644 index 00000000..1dcc49c2 Binary files /dev/null and b/spreadsheet_quotation/static/description/icon.png differ diff --git a/spreadsheet_quotation/static/description/index.html b/spreadsheet_quotation/static/description/index.html new file mode 100644 index 00000000..45b7a3f4 --- /dev/null +++ b/spreadsheet_quotation/static/description/index.html @@ -0,0 +1,496 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Spreadsheet Quotation Calculator

+ +

Alpha License: AGPL-3 OCA/spreadsheet Translate me on Weblate Try me on Runboat

+

This module allows linking spreadsheet calculators to quotation +templates in Odoo. When a sale order is created from a template that has +a calculator, a copy of the spreadsheet is automatically assigned to the +order with a pre-configured global filter so the ODOO.LIST formulas +display only that order’s lines.

+

The spreadsheet calculator is built on top of spreadsheet_oca and +uses ODOO.LIST formulas to display sale order line data (product, +quantity, unit price, etc.). Users can add custom formulas, +calculations, and charts to build complex pricing logic.

+

A Field Sync side panel lets users map spreadsheet columns to sale +order line fields. This column-based approach is more intuitive than +cell-by-cell mapping and makes it easy to push calculated values back to +the sale order.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

Setting up a quotation calculator

+
+1. Go to **Sales > Configuration > Quotation Templates**.
+2. Open or create a quotation template.
+3. Click **Create Calculator** next to the "Quotation Calculator" field.
+4. A wizard will open; set a name and number of initial rows, then click
+   **Create Calculator**.
+5. The spreadsheet editor opens with a pre-configured list of
+   ``sale.order.line`` fields.
+6. Customize the spreadsheet: add formulas, charts, or extra columns as
+   needed.
+7. Save the spreadsheet.
+
+Using the calculator on a sale order
+
+
    +
  1. Create a new quotation and select the template that has a calculator.
  2. +
  3. A Calculator smart button appears on the sale order form.
  4. +
  5. Click it to open the spreadsheet filtered to the current order’s +lines.
  6. +
  7. Edit values in the spreadsheet.
  8. +
  9. Use File > Field Sync to map columns to sale order line fields.
  10. +
  11. Save the spreadsheet from the editor.
  12. +
+

Syncing values back to the order

+
+After configuring field sync mappings in the spreadsheet, the mapped
+column values can be pushed to the corresponding sale order line fields
+when saving the spreadsheet or from the sale order form.
+
+Step-by-step guide
+~~~~~~~~~~~~~~~~~~
+
+[1. Add quotation templates in settings](image1.png)
+[2. Create a new template & create a calculator](image2.png)
+[3. Map Sync fields > columns to fields](image3.png)
+[4. Save template > create a quotation from template and open calculator](image4.png)
+[5. add new lines to the calculator, save and sync](image5.png)
+[6. Add new products if any](image6.png)
+[7. Quotation updated!!!](image7.png)
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Cloud Lotus
  • +
+
+
+

Contributors

+
    +
  • OCA Contributors
  • +
  • Cloud Lotus
      +
    • Cifuentes Roberto
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/spreadsheet project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js new file mode 100644 index 00000000..11dd81b2 --- /dev/null +++ b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_plugin.esm.js @@ -0,0 +1,107 @@ +/** @odoo-module **/ + +import {CorePlugin, coreTypes, registries} from "@odoo/o-spreadsheet"; +import {_t} from "@web/core/l10n/translation"; + +const {corePluginRegistry, topbarMenuRegistry} = registries; + +coreTypes.add("UPDATE_FIELD_SYNC_CONFIG"); +coreTypes.add("ADD_FIELD_SYNC_MAPPING"); +coreTypes.add("REMOVE_FIELD_SYNC_MAPPING"); + +/** + * Plugin that stores field-sync configuration in the spreadsheet data. + * + * The configuration maps spreadsheet columns to sale.order.line fields, + * allowing the user to sync computed values back to the sale order. + * + * The mappings are stored as a list of {column, field} objects where + * `column` is the 0-based column index and `field` is the technical + * field name on sale.order.line. + */ +export class FieldSyncCorePlugin extends CorePlugin { + static getters = /** @type {const} */ ([ + "getFieldSyncConfig", + "getFieldSyncMappings", + "getFieldSyncListId", + ]); + + constructor(config) { + super(config); + this.fieldSyncConfig = { + listId: "1", + mappings: [], + }; + } + + allowDispatch(cmd) { + switch (cmd.type) { + case "UPDATE_FIELD_SYNC_CONFIG": + case "ADD_FIELD_SYNC_MAPPING": + case "REMOVE_FIELD_SYNC_MAPPING": + return "Success"; + } + return "Success"; + } + + handle(cmd) { + switch (cmd.type) { + case "UPDATE_FIELD_SYNC_CONFIG": + this.history.update("fieldSyncConfig", cmd.config); + break; + case "ADD_FIELD_SYNC_MAPPING": { + const mappings = [...this.fieldSyncConfig.mappings, cmd.mapping]; + this.history.update("fieldSyncConfig", { + ...this.fieldSyncConfig, + mappings, + }); + break; + } + case "REMOVE_FIELD_SYNC_MAPPING": { + const mappings = this.fieldSyncConfig.mappings.filter( + (_, idx) => idx !== cmd.index + ); + this.history.update("fieldSyncConfig", { + ...this.fieldSyncConfig, + mappings, + }); + break; + } + } + } + + getFieldSyncConfig() { + return this.fieldSyncConfig; + } + + getFieldSyncMappings() { + return this.fieldSyncConfig.mappings || []; + } + + getFieldSyncListId() { + return this.fieldSyncConfig.listId || "1"; + } + + import(data) { + if (data.fieldSyncConfig) { + this.fieldSyncConfig = data.fieldSyncConfig; + } + } + + export(data) { + data.fieldSyncConfig = this.fieldSyncConfig; + } +} + +corePluginRegistry.add("FieldSyncCorePlugin", FieldSyncCorePlugin); + +topbarMenuRegistry.addChild("field_sync", ["file"], { + name: _t("Field Sync"), + sequence: 80, + execute: (env) => env.openSidePanel("FieldSyncPanel", {}), + icon: "o-spreadsheet-Icon.EDIT", + isVisible: (env) => { + const listIds = env.model.getters.getListIds(); + return listIds.length > 0; + }, +}); diff --git a/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js new file mode 100644 index 00000000..c554d7cc --- /dev/null +++ b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.esm.js @@ -0,0 +1,451 @@ +/** @odoo-module **/ + +import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog"; +import {registries} from "@odoo/o-spreadsheet"; +import {_t} from "@web/core/l10n/translation"; +import {useService} from "@web/core/utils/hooks"; + +const {sidePanelRegistry} = registries; +const {Component, useState, onWillStart} = owl; + +const SYNCABLE_FIELDS = [ + {name: "product_id", label: _t("Product"), type: "many2one"}, + {name: "name", label: _t("Description"), type: "char"}, + {name: "product_uom_qty", label: _t("Quantity"), type: "float"}, + {name: "price_unit", label: _t("Unit Price"), type: "float"}, + {name: "discount", label: _t("Discount (%)"), type: "float"}, +]; + +export class FieldSyncPanel extends Component { + static template = "spreadsheet_quotation.FieldSyncPanel"; + static props = {}; + + setup() { + this.orm = useService("orm"); + this.notification = useService("notification"); + this.dialog = useService("dialog"); + this.availableColumns = []; + this.syncableFields = SYNCABLE_FIELDS; + this.state = useState({ + newColumn: "", + newField: "", + saving: false, + }); + onWillStart(async () => { + this._loadAvailableColumns(); + }); + } + + get mappings() { + try { + return this.env.model.getters.getFieldSyncMappings(); + } catch { + return []; + } + } + + get listId() { + try { + return this.env.model.getters.getFieldSyncListId(); + } catch { + return "1"; + } + } + + get hasSOList() { + try { + const id = this.listId; + if (!id || !this.env.model.getters.isExistingList(id)) { + return false; + } + const def = this.env.model.getters.getListDefinition(id); + return def.model === "sale.order.line"; + } catch { + return false; + } + } + + get orderId() { + try { + const filters = this.env.model.getters.getGlobalFilters(); + for (const filter of filters) { + if (filter.type === "relation" && filter.modelName === "sale.order") { + const value = this.env.model.getters.getGlobalFilterValue( + filter.id + ); + if (value && value.length > 0) { + return value[0]; + } + const defaultValue = + this.env.model.getters.getGlobalFilterDefaultValue(filter.id); + if (defaultValue && defaultValue.length > 0) { + return defaultValue[0]; + } + } + } + } catch { + // Filter getters not available + } + return null; + } + + get hasProductMapping() { + return this.mappings.some((m) => m.field === "product_id"); + } + + _loadAvailableColumns() { + try { + const sheetId = this.env.model.getters.getActiveSheetId(); + const numberOfCols = this.env.model.getters.getNumberCols(sheetId); + for (let i = 0; i < numberOfCols && i < 26; i++) { + this.availableColumns.push(String.fromCharCode(65 + i)); + } + } catch { + for (let i = 0; i < 10; i++) { + this.availableColumns.push(String.fromCharCode(65 + i)); + } + } + } + + getFieldLabel(fieldName) { + const field = this.syncableFields.find((f) => f.name === fieldName); + return field ? field.label.toString() : fieldName; + } + + addMapping() { + if (!this.state.newColumn || !this.state.newField) { + return; + } + const existing = this.mappings.find((m) => m.column === this.state.newColumn); + if (existing) { + this.notification.add(_t("This column already has a mapping"), { + type: "warning", + }); + return; + } + const existingField = this.mappings.find( + (m) => m.field === this.state.newField + ); + if (existingField) { + this.notification.add(_t("This field already has a mapping"), { + type: "warning", + }); + return; + } + this.env.model.dispatch("ADD_FIELD_SYNC_MAPPING", { + mapping: { + column: this.state.newColumn, + field: this.state.newField, + }, + }); + this.state.newColumn = ""; + this.state.newField = ""; + } + + removeMapping(index) { + this.env.model.dispatch("REMOVE_FIELD_SYNC_MAPPING", {index}); + } + + /** + * Convert a column letter (A, B, ..., Z) to a 0-based index. + */ + _colLetterToIndex(letter) { + let index = 0; + for (let i = 0; i < letter.length; i++) { + index = index * 26 + (letter.charCodeAt(i) - 64); + } + return index - 1; + } + + /** + * Read one spreadsheet row and return the SO line ID (if it maps to an + * existing ODOO.LIST record), the mapped field values, and a flag + * indicating whether the row contains any data worth syncing. + */ + _readRow(sheetId, row) { + const result = {lineId: null, values: {}, productName: null, hasData: false}; + + try { + const listPosition = row - 1; + const idResult = this.env.model.getters.getListCellValueAndFormat( + this.listId, + listPosition, + "id" + ); + if (idResult && idResult.value && typeof idResult.value === "number") { + result.lineId = idResult.value; + } + } catch { + // No list record at this position + } + + for (const mapping of this.mappings) { + const colIdx = this._colLetterToIndex(mapping.column); + const cell = this.env.model.getters.getEvaluatedCell({ + sheetId, + col: colIdx, + row, + }); + if ( + cell.type !== "empty" && + cell.value !== undefined && + cell.value !== "" + ) { + result.hasData = true; + if (mapping.field === "product_id") { + result.productName = String(cell.value); + } else { + result.values[mapping.field] = cell.value; + } + } + } + + if (result.lineId) { + result.hasData = true; + } + return result; + } + + /** + * Search for a product by name using name_search (handles display_name, + * internal reference, etc.). Returns the product ID or null. + */ + async _findProduct(name) { + if (!name) { + return null; + } + const exact = await this.orm.call("product.product", "name_search", [], { + name, + operator: "=", + limit: 1, + }); + if (exact.length) { + return exact[0][0]; + } + const fuzzy = await this.orm.call("product.product", "name_search", [], { + name, + operator: "ilike", + limit: 1, + }); + if (fuzzy.length) { + return fuzzy[0][0]; + } + return null; + } + + _getRpcErrorMessage(error) { + return error.data?.message || error.message || String(error); + } + + /** + * Ensure the linked sale order allows spreadsheet synchronization. + */ + async _ensureOrderSyncAllowed(orderId) { + const [order] = await this.orm.read("sale.order", [orderId], ["state"]); + if (order.state === "sale" || order.state === "cancel") { + const message = + order.state === "cancel" + ? _t("Cannot sync a cancelled quotation.") + : _t("Cannot sync a confirmed sales order."); + this.notification.add(message, {type: "warning"}); + return false; + } + return true; + } + + /** + * Show a confirmation dialog listing the products that will be created. + * Resolves to true when the user confirms, false otherwise. + */ + _confirmCreateProducts(productNames) { + return new Promise((resolve) => { + const list = productNames.map((n) => ` • ${n}`).join("\n"); + const body = + _t( + "The following products were not found in the catalog and will be created:" + ) + + "\n\n" + + list + + "\n\n" + + _t("Do you want to create them?"); + + this.dialog.add(ConfirmationDialog, { + title: _t("Create New Products"), + body, + confirmLabel: _t("Create and Sync"), + confirm: () => resolve(true), + cancel: () => resolve(false), + }); + }); + } + + /** + * Main sync entry-point. + * + * 1. Scan spreadsheet rows for data (existing lines and new rows). + * 2. Resolve product names to IDs (via name_search). + * 3. If products are missing, ask the user for confirmation to create them. + * 4. Build ORM x2many commands and write to the sale order. + */ + async saveToSaleOrder() { + if (!this.hasSOList) { + this.notification.add(_t("No sale order line list found"), { + type: "warning", + }); + return; + } + const orderId = this.orderId; + if (!orderId) { + this.notification.add( + _t("No sale order linked. Set the Sale Order filter first."), + {type: "warning"} + ); + return; + } + if (!this.mappings.length) { + this.notification.add(_t("No field mappings configured"), { + type: "warning", + }); + return; + } + + this.state.saving = true; + try { + if (!(await this._ensureOrderSyncAllowed(orderId))) { + return; + } + await this.env.saveSpreadsheet(); + + const sheetId = this.env.model.getters.getActiveSheetId(); + const listId = this.listId; + + const listDataSource = this.env.model.getters.getListDataSource(listId); + if (listDataSource && !listDataSource.isReady()) { + await listDataSource.load(); + } + + // ---- 1. Scan rows ---- + const existingUpdates = {}; + const newRows = []; + const maxRow = this.env.model.getters.getNumberRows(sheetId); + let emptyStreak = 0; + + for (let row = 1; row < maxRow && emptyStreak < 5; row++) { + const rowData = this._readRow(sheetId, row); + if (!rowData.hasData) { + emptyStreak++; + continue; + } + emptyStreak = 0; + + if (rowData.lineId) { + existingUpdates[rowData.lineId] = { + values: rowData.values, + productName: rowData.productName, + }; + } else if ( + rowData.productName || + Object.keys(rowData.values).length > 0 + ) { + newRows.push({ + values: rowData.values, + productName: rowData.productName, + }); + } + } + + // ---- 2. Resolve product names to IDs ---- + const allProductNames = new Set(); + for (const data of Object.values(existingUpdates)) { + if (data.productName) { + allProductNames.add(data.productName); + } + } + for (const row of newRows) { + if (row.productName) { + allProductNames.add(row.productName); + } + } + + const productMap = {}; + for (const name of allProductNames) { + const productId = await this._findProduct(name); + if (productId) { + productMap[name] = productId; + } + } + + // ---- 3. Handle missing products ---- + const missingProducts = [...allProductNames].filter((n) => !productMap[n]); + + if (missingProducts.length > 0) { + const confirmed = await this._confirmCreateProducts(missingProducts); + if (!confirmed) { + this.notification.add(_t("Sync cancelled"), {type: "info"}); + return; + } + for (const name of missingProducts) { + const ids = await this.orm.create("product.product", [{name}]); + productMap[name] = ids[0]; + } + } + + // ---- 4. Build ORM commands ---- + const writeCommands = []; + + for (const [lineId, data] of Object.entries(existingUpdates)) { + const vals = {...data.values}; + if (data.productName && productMap[data.productName]) { + vals.product_id = productMap[data.productName]; + } + if (Object.keys(vals).length > 0) { + writeCommands.push([1, parseInt(lineId), vals]); + } + } + + for (const row of newRows) { + const vals = {...row.values}; + if (row.productName && productMap[row.productName]) { + vals.product_id = productMap[row.productName]; + } + if (vals.product_id || Object.keys(vals).length > 0) { + writeCommands.push([0, 0, vals]); + } + } + + // ---- 5. Execute ---- + if (writeCommands.length) { + await this.orm.call( + "sale.order", + "action_sync_spreadsheet_order_lines", + [[orderId], writeCommands] + ); + + const updated = writeCommands.filter((c) => c[0] === 1).length; + const created = writeCommands.filter((c) => c[0] === 0).length; + const parts = []; + if (updated) { + parts.push(_t("%s line(s) updated", updated)); + } + if (created) { + parts.push(_t("%s line(s) created", created)); + } + this.notification.add(parts.join(", "), {type: "success"}); + } else { + this.notification.add(_t("No values to sync"), {type: "info"}); + } + } catch (e) { + this.notification.add( + _t("Error syncing: %s", this._getRpcErrorMessage(e)), + {type: "danger"} + ); + } finally { + this.state.saving = false; + } + } +} + +sidePanelRegistry.add("FieldSyncPanel", { + title: _t("Field Sync"), + Body: FieldSyncPanel, +}); diff --git a/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml new file mode 100644 index 00000000..1f0f70e6 --- /dev/null +++ b/spreadsheet_quotation/static/src/quotation_spreadsheet/field_sync_side_panel.xml @@ -0,0 +1,140 @@ + + + + +
+
+
Column Mappings
+

+ Map spreadsheet columns to sale order line fields. + Existing lines will be updated in place; new rows with + product data will be added as new quotation lines. +

+
+ +
+
+ No sale order line list found in this spreadsheet. +
+
+ + +
+
+ + Linked to Sale Order # +
+
+
+
+ Set the Sale Order filter to link this calculator to an order. +
+
+ +
+ + + + + + + + + + + + + + + + +
ColumnField +
+ + + + + +
+
+ +
+

No mappings configured yet.

+
+ +
+
Add Mapping
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + Product sync enabled. + New rows with a product name will be added as new + quotation lines. If a product is not found in the + catalog you will be asked to confirm its creation. +
+
+ +
+ +
+
+
+
+ +
diff --git a/spreadsheet_quotation/static/src/quotation_spreadsheet/quotation_spreadsheet.xml b/spreadsheet_quotation/static/src/quotation_spreadsheet/quotation_spreadsheet.xml new file mode 100644 index 00000000..51c9fb43 --- /dev/null +++ b/spreadsheet_quotation/static/src/quotation_spreadsheet/quotation_spreadsheet.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spreadsheet_quotation/tests/__init__.py b/spreadsheet_quotation/tests/__init__.py new file mode 100644 index 00000000..a26a5545 --- /dev/null +++ b/spreadsheet_quotation/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_spreadsheet_quotation diff --git a/spreadsheet_quotation/tests/test_spreadsheet_quotation.py b/spreadsheet_quotation/tests/test_spreadsheet_quotation.py new file mode 100644 index 00000000..7d9ff531 --- /dev/null +++ b/spreadsheet_quotation/tests/test_spreadsheet_quotation.py @@ -0,0 +1,275 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSpreadsheetQuotation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "list_price": 100.0, + } + ) + cls.template = cls.env["sale.order.template"].create( + { + "name": "Test Template", + "sale_order_template_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + "product_uom_qty": 5, + "product_uom_id": cls.product.uom_id.id, + }, + ) + ], + } + ) + + def _create_calculator(self, template=None, name="My Calculator", lines=10): + template = template or self.template + wizard = ( + self.env["spreadsheet.quotation.create"] + .with_context(default_sale_order_template_id=template.id) + .create( + { + "name": name, + "line_count": lines, + "sale_order_template_id": template.id, + } + ) + ) + return wizard.action_create() + + def test_wizard_creates_calculator(self): + """The wizard should create a spreadsheet linked to the template.""" + self.assertFalse(self.template.spreadsheet_id) + action = self._create_calculator() + + self.assertTrue(self.template.spreadsheet_id) + self.assertEqual(self.template.spreadsheet_id.name, "My Calculator") + self.assertEqual(action["type"], "ir.actions.client") + self.assertEqual(action["tag"], "action_spreadsheet_oca") + + def test_wizard_spreadsheet_contains_list(self): + """The spreadsheet should contain a list definition for sale.order.line.""" + self._create_calculator() + + data = self.template.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + self.assertIn("lists", data) + list_def = data["lists"].get("1") + self.assertIsNotNone(list_def) + self.assertEqual(list_def["model"], "sale.order.line") + self.assertIn("product_id", list_def["columns"]) + self.assertIn("product_uom_qty", list_def["columns"]) + self.assertIn("price_unit", list_def["columns"]) + + def test_wizard_list_filters_display_type(self): + """The list domain should exclude section/note lines.""" + self._create_calculator() + + data = self.template.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + list_def = data["lists"]["1"] + self.assertIn(["display_type", "=", False], list_def["domain"]) + + def test_wizard_spreadsheet_contains_global_filter(self): + """The spreadsheet should contain a global filter for sale.order.""" + self._create_calculator() + + data = self.template.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + self.assertIn("globalFilters", data) + filters = data["globalFilters"] + self.assertEqual(len(filters), 1) + self.assertEqual(filters[0]["type"], "relation") + self.assertEqual(filters[0]["modelName"], "sale.order") + + def test_wizard_field_matching(self): + """The list should have field matching linking it to the global filter.""" + self._create_calculator() + + data = self.template.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + list_def = data["lists"]["1"] + filter_id = data["globalFilters"][0]["id"] + self.assertIn(filter_id, list_def["fieldMatching"]) + matching = list_def["fieldMatching"][filter_id] + self.assertEqual(matching["chain"], "order_id") + self.assertEqual(matching["type"], "many2one") + + def test_wizard_spreadsheet_cells_have_formulas(self): + """The spreadsheet should contain ODOO.LIST formulas in cells.""" + self._create_calculator(lines=5) + + data = self.template.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + sheets = data.get("sheets", []) + self.assertTrue(sheets) + cells = sheets[0].get("cells", {}) + self.assertIn("A1", cells) + self.assertIn("ODOO.LIST.HEADER", cells["A1"]["content"]) + self.assertIn("A2", cells) + self.assertIn("ODOO.LIST", cells["A2"]["content"]) + + def test_template_application_copies_spreadsheet(self): + """Applying a template with a calculator to a SO should copy it.""" + self._create_calculator() + + order = self.env["sale.order"].create({"partner_id": self.partner.id}) + order.sale_order_template_id = self.template + order._onchange_sale_order_template_id() + order._onchange_sale_order_template_id_spreadsheet() + + self.assertTrue(order.spreadsheet_id) + self.assertNotEqual(order.spreadsheet_id, self.template.spreadsheet_id) + + def test_copied_spreadsheet_has_filter_with_order_id(self): + """The copied spreadsheet should have the global filter set to the SO id.""" + self._create_calculator() + + order = self.env["sale.order"].create({"partner_id": self.partner.id}) + order.sale_order_template_id = self.template + order._onchange_sale_order_template_id() + order._onchange_sale_order_template_id_spreadsheet() + + data = order.spreadsheet_id.spreadsheet_raw + if isinstance(data, str): + data = json.loads(data) + + filters = data.get("globalFilters", []) + so_filter = next( + (f for f in filters if f.get("modelName") == "sale.order"), + None, + ) + self.assertIsNotNone(so_filter) + self.assertEqual(so_filter["defaultValue"], [order.id]) + + def test_template_without_spreadsheet(self): + """Applying a template without calculator should not create one.""" + template_no_calc = self.env["sale.order.template"].create( + {"name": "No Calculator Template"} + ) + order = self.env["sale.order"].create({"partner_id": self.partner.id}) + order.sale_order_template_id = template_no_calc + order._onchange_sale_order_template_id() + order._onchange_sale_order_template_id_spreadsheet() + + self.assertFalse(order.spreadsheet_id) + + def test_clearing_template_clears_spreadsheet(self): + """Removing the template should clear the spreadsheet reference.""" + self._create_calculator() + + order = self.env["sale.order"].create({"partner_id": self.partner.id}) + order.sale_order_template_id = self.template + order._onchange_sale_order_template_id() + order._onchange_sale_order_template_id_spreadsheet() + self.assertTrue(order.spreadsheet_id) + + order.sale_order_template_id = False + order._onchange_sale_order_template_id_spreadsheet() + self.assertFalse(order.spreadsheet_id) + + def test_open_spreadsheet_action(self): + """Opening the calculator should return a client action.""" + self._create_calculator() + + action = self.template.action_open_spreadsheet_calculator() + self.assertEqual(action["type"], "ir.actions.client") + self.assertEqual(action["tag"], "action_spreadsheet_oca") + + def test_col_index_to_letter(self): + """Column index conversion should produce correct letters.""" + wizard = self.env["spreadsheet.quotation.create"] + self.assertEqual(wizard._col_index_to_letter(0), "A") + self.assertEqual(wizard._col_index_to_letter(1), "B") + self.assertEqual(wizard._col_index_to_letter(25), "Z") + self.assertEqual(wizard._col_index_to_letter(26), "AA") + self.assertEqual(wizard._col_index_to_letter(27), "AB") + + def test_has_spreadsheet_computed(self): + """has_spreadsheet should reflect the presence of spreadsheet_id.""" + order = self.env["sale.order"].create({"partner_id": self.partner.id}) + self.assertFalse(order.has_spreadsheet) + + self._create_calculator() + + order.sale_order_template_id = self.template + order._onchange_sale_order_template_id() + order._onchange_sale_order_template_id_spreadsheet() + + self.assertTrue(order.has_spreadsheet) + + def _create_order_with_line(self): + return self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 1, + }, + ) + ], + } + ) + + def test_sync_allowed_in_draft(self): + """Spreadsheet sync should update lines on draft quotations.""" + order = self._create_order_with_line() + line = order.order_line[0] + commands = [[1, line.id, {"product_uom_qty": 10}]] + order.action_sync_spreadsheet_order_lines(commands) + self.assertEqual(line.product_uom_qty, 10) + + def test_sync_allowed_in_sent(self): + """Spreadsheet sync should update lines on sent quotations.""" + order = self._create_order_with_line() + order.write({"state": "sent"}) + line = order.order_line[0] + commands = [[1, line.id, {"product_uom_qty": 8}]] + order.action_sync_spreadsheet_order_lines(commands) + self.assertEqual(line.product_uom_qty, 8) + + def test_sync_blocked_in_sale(self): + """Spreadsheet sync should be blocked on confirmed sales orders.""" + order = self._create_order_with_line() + order.action_confirm() + line = order.order_line[0] + commands = [[1, line.id, {"product_uom_qty": 10}]] + with self.assertRaises(UserError): + order.action_sync_spreadsheet_order_lines(commands) + + def test_sync_blocked_in_cancel(self): + """Spreadsheet sync should be blocked on cancelled quotations.""" + order = self._create_order_with_line() + order.action_cancel() + line = order.order_line[0] + commands = [[1, line.id, {"product_uom_qty": 10}]] + with self.assertRaises(UserError): + order.action_sync_spreadsheet_order_lines(commands) diff --git a/spreadsheet_quotation/views/sale_order_template_views.xml b/spreadsheet_quotation/views/sale_order_template_views.xml new file mode 100644 index 00000000..90b32ae3 --- /dev/null +++ b/spreadsheet_quotation/views/sale_order_template_views.xml @@ -0,0 +1,31 @@ + + + + sale.order.template.form.spreadsheet + sale.order.template + + + + + + + + + + diff --git a/spreadsheet_quotation/views/sale_order_views.xml b/spreadsheet_quotation/views/sale_order_views.xml new file mode 100644 index 00000000..14f223b7 --- /dev/null +++ b/spreadsheet_quotation/views/sale_order_views.xml @@ -0,0 +1,25 @@ + + + + sale.order.form.spreadsheet + sale.order + + +
+ + + + + +
+
+
+
diff --git a/spreadsheet_quotation/wizards/__init__.py b/spreadsheet_quotation/wizards/__init__.py new file mode 100644 index 00000000..16c9bd45 --- /dev/null +++ b/spreadsheet_quotation/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import spreadsheet_quotation_create diff --git a/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py b/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py new file mode 100644 index 00000000..c94cf96d --- /dev/null +++ b/spreadsheet_quotation/wizards/spreadsheet_quotation_create.py @@ -0,0 +1,135 @@ +# Copyright 2026 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import uuid + +from odoo import _, api, fields, models + +DEFAULT_COLUMNS = [ + "product_id", + "product_uom_qty", + "qty_delivered", + "qty_invoiced", + "qty_to_invoice", + "product_uom_id", + "price_unit", + "price_tax", + "price_subtotal", +] + +DEFAULT_LINE_COUNT = 20 + + +class SpreadsheetQuotationCreate(models.TransientModel): + _name = "spreadsheet.quotation.create" + _description = "Create Quotation Calculator Spreadsheet" + + sale_order_template_id = fields.Many2one( + "sale.order.template", + required=True, + readonly=True, + ) + name = fields.Char( + default="Quotation Calculator", + required=True, + ) + line_count = fields.Integer( + string="Number of lines", + default=DEFAULT_LINE_COUNT, + help="Initial number of rows to display in the list.", + ) + + def action_create(self): + self.ensure_one() + spreadsheet_data = self._build_spreadsheet_data() + spreadsheet = self.env["spreadsheet.spreadsheet"].create( + { + "name": self.name, + "spreadsheet_raw": spreadsheet_data, + } + ) + self.sale_order_template_id.spreadsheet_id = spreadsheet + return spreadsheet.open_spreadsheet() + + def _build_spreadsheet_data(self): + columns = DEFAULT_COLUMNS + line_count = self.line_count or DEFAULT_LINE_COUNT + filter_id = str(uuid.uuid4()) + list_id = "1" + sheet_id = "sheet1" + + cells = self._build_cells(list_id, columns, line_count) + col_count = len(columns) + + lang = self.env["res.lang"]._lang_get(self.env.user.lang) + locale = lang._odoo_lang_to_spreadsheet_locale() + + return { + "version": 1, + "sheets": [ + { + "id": sheet_id, + "name": _("Sale Order Lines"), + "cells": cells, + "colNumber": max(col_count, 10), + "rowNumber": max(line_count + 2, 30), + } + ], + "settings": {"locale": locale}, + "revisionId": "START_REVISION", + "lists": { + list_id: { + "columns": columns, + "domain": [["display_type", "=", False]], + "model": "sale.order.line", + "context": {}, + "orderBy": [], + "id": list_id, + "name": _("Sale Order Lines"), + "fieldMatching": { + filter_id: { + "chain": "order_id", + "type": "many2one", + } + }, + } + }, + "listNextId": 2, + "globalFilters": [ + { + "id": filter_id, + "type": "relation", + "label": _("Sale Order"), + "modelName": "sale.order", + "defaultValue": [], + } + ], + } + + @api.model + def _build_cells(self, list_id, columns, line_count): + cells = {} + for col_idx, col_name in enumerate(columns): + col_letter = self._col_index_to_letter(col_idx) + cells[f"{col_letter}1"] = { + "content": f'=ODOO.LIST.HEADER({list_id},"{col_name}")', + } + for row in range(1, line_count + 1): + cells[f"{col_letter}{row + 1}"] = { + "content": f'=ODOO.LIST({list_id},{row},"{col_name}")', + } + return cells + + @api.model + def _col_index_to_letter(self, idx): + """Convert a 0-based column index to spreadsheet letters. + + Examples: A, B, ..., Z, AA, ... + """ + result = "" + while True: + result = chr(ord("A") + idx % 26) + result + idx = idx // 26 - 1 + if idx < 0: + break + return result diff --git a/spreadsheet_quotation/wizards/spreadsheet_quotation_create.xml b/spreadsheet_quotation/wizards/spreadsheet_quotation_create.xml new file mode 100644 index 00000000..c11d8d65 --- /dev/null +++ b/spreadsheet_quotation/wizards/spreadsheet_quotation_create.xml @@ -0,0 +1,26 @@ + + + + spreadsheet.quotation.create.form + spreadsheet.quotation.create + + + + + + + + + + + + +