diff --git a/sale_timesheet_rounded/README.rst b/sale_timesheet_rounded/README.rst new file mode 100644 index 000000000..6b26613fc --- /dev/null +++ b/sale_timesheet_rounded/README.rst @@ -0,0 +1,136 @@ +====================== +Sale Timesheet Rounded +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1978ba49c441473463786ded6958a21b6876f6b317a72dde30b11512afa456a8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ftimesheet-lightgray.png?logo=github + :target: https://github.com/OCA/timesheet/tree/17.0/sale_timesheet_rounded + :alt: OCA/timesheet +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/timesheet-17-0/timesheet-17-0-sale_timesheet_rounded + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/timesheet&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Round timesheet lines amounts in sales based on project' settings. + +A typical use case is: you work 5 minutes but you want to invoice 15 +minutes. + +With this module you can configure a rounding unit or factor on the +project and all the lines tracked on this project's tasks will show a +rounded amount. + +If you want you can override the value manually on each entry. + +The delivered quantity on the sale order line - and by consequence on +the invoice - will be computed using the rounded amount. Therefore, +expense lines and other non-timesheet lines will be updated with a +rounded amount that is equal to the amount. + +WARNING: This module cannot be used with timesheet_grid without further +adapation as an update of an existing timesheet line will NOT update the +rounded amount. To achieve this, you need to override adjust_grid +function to pass the force_compute context key. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to a project and set the following fields according to your needs: + +- Timesheet rounding unit + +Defines the rounding unit. For instance, if you want to round to 1 hour, +you can set 1.0. If you want to round to 15 min set 0.25. + +- Timesheet rounding method + +Options: "No" (default), "Closest", "Up", "Down". + +Please refer to odoo.tools.float_utils.float_round to understand the +difference. + +- Timesheet rounding factor (percentage) + +When round unit is not defined you can round by a fixed %. + +When using both a unit and a factor, the factor will be applied first: + + result = round(amount \* percentage, unit) + +Known issues / Roadmap +====================== + +- improve test coverage + +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 +------- + +* Camptocamp + +Contributors +------------ + +- Simone Orsi +- Thomas Nowicki +- Akim Juillerat +- Foram Shah +- Phuc Kieu +- Son Ho + +Other credits +------------- + +The migration of this sale_timesheet_rounded from 16.0 to 17.0 was +financially supported by Camptocamp + +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/timesheet `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_timesheet_rounded/__init__.py b/sale_timesheet_rounded/__init__.py new file mode 100644 index 000000000..38143ad4e --- /dev/null +++ b/sale_timesheet_rounded/__init__.py @@ -0,0 +1,3 @@ +from . import models +from .hooks import pre_init_hook +from . import wizard diff --git a/sale_timesheet_rounded/__manifest__.py b/sale_timesheet_rounded/__manifest__.py new file mode 100644 index 000000000..71256cbee --- /dev/null +++ b/sale_timesheet_rounded/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2019 Camptocamp SA +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Sale Timesheet Rounded", + "summary": "Round timesheet entries amount based on project settings.", + "version": "17.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Sales", + "website": "https://github.com/OCA/timesheet", + "depends": ["project", "hr_timesheet", "sale_timesheet"], + "data": [ + # Views + "views/account_analytic_line.xml", + "views/project_project.xml", + "views/project_task.xml", + ], + "installable": True, + "pre_init_hook": "pre_init_hook", +} diff --git a/sale_timesheet_rounded/hooks.py b/sale_timesheet_rounded/hooks.py new file mode 100644 index 000000000..abeb7fd1a --- /dev/null +++ b/sale_timesheet_rounded/hooks.py @@ -0,0 +1,28 @@ +# Copyright 2019 Camptocamp SA +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +import logging + +from psycopg2 import sql + +_logger = logging.getLogger(__name__) + + +def pre_init_hook(env): + """Initialize the value of the given column for existing rows in a fast way.""" + _logger.info( + "Initializing column `unit_amount_rounded` with the " "value of `unit_amount`" + ) + table = sql.Identifier("account_analytic_line") + column = sql.Identifier("unit_amount_rounded") + env.cr.execute( # pylint: disable=E8103 + sql.SQL("ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} NUMERIC").format( + table, column + ) + ) + env.cr.execute( # pylint: disable=E8103 + sql.SQL( + "UPDATE {table} SET {column} = unit_amount WHERE {column} IS NULL" + ).format(table=table, column=column) + ) diff --git a/sale_timesheet_rounded/i18n/de.po b/sale_timesheet_rounded/i18n/de.po new file mode 100644 index 000000000..769a723d9 --- /dev/null +++ b/sale_timesheet_rounded/i18n/de.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_rounded +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2019-09-26 14:24+0000\n" +"Last-Translator: Akim Juillerat \n" +"Language-Team: none\n" +"Language: de\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 3.8\n" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "" +"1.0 = hour\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " +msgstr "" +"1.0 = Stunde\n" +" 0.25 = 15 Min\n" +" 0.084 ~= 5 Min\n" +" 0.017 ~= 1 Min\n" +" " + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_kanban_inherit +#, fuzzy +msgid "" +"
\n" +" Rounded: " +msgstr "" +"
\n" +" Gerundet: " + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_account_analytic_line +msgid "Analytic Line" +msgstr "Kostenstellen Buchung" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__half_up +msgid "Closest" +msgstr "Am nächsten" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__display_name +msgid "Display Name" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__down +msgid "Down" +msgstr "Nach unten" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.hr_timesheet_view_task_form2_inherited_inherit +msgid "Duration (rounded)" +msgstr "Geleistete Stunden (gerundet)" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__id +msgid "ID" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "" +"If you activate the rounding of timesheet lines, only new entries will be " +"rounded (i.e. existing lines will not be rounded automatically)." +msgstr "" +"Wenn Sie die Rundung von Zeiterfassungszeilen aktivieren, werden nur neue " +"Einträge gerundet (d.h. bestehende Zeilen werden nicht automatisch gerundet)." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__no +msgid "No rounding" +msgstr "Keine Rundung" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_project_project +msgid "Project" +msgstr "Projekt" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.view_account_analytic_line_form_inherit +msgid "Quantity Rounded" +msgstr "Gerundete Menge" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__unit_amount_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_hr_timesheet_switch__unit_amount_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Quantity rounded" +msgstr "Gerundete Menge" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_sale_order_line +msgid "Sales Order Line" +msgstr "Auftragzeile" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.project_project_form_inherit +msgid "Time rounding" +msgstr "Zeit Rundung" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_factor +msgid "Timesheet rounding factor in percentage" +msgstr "Stundenzettel Rundungsfaktor (in Prozent)" + +#. module: sale_timesheet_rounded +#: model:ir.model.constraint,message:sale_timesheet_rounded.constraint_project_project_check_timesheet_rounding_factor +msgid "" +"Timesheet rounding factor should stay between 0 and 500, endpoints included." +msgstr "" +"Der Stundezettel Rundungsfaktor muss inzwischen 0 und 500 sein, Endpunkte " +"inklusive." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "Timesheet rounding method" +msgstr "Stundenzettel Rundungsmethode" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "Timesheet rounding unit" +msgstr "Stundenzettel Rundungseinheit" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Total quantity rounded" +msgstr "Total gerundete Menge" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__up +msgid "Up" +msgstr "Nach oben" diff --git a/sale_timesheet_rounded/i18n/es.po b/sale_timesheet_rounded/i18n/es.po new file mode 100644 index 000000000..e305ff108 --- /dev/null +++ b/sale_timesheet_rounded/i18n/es.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_rounded +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-05-18 20:19+0000\n" +"Last-Translator: Josep M \n" +"Language-Team: none\n" +"Language: es\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 3.10\n" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "" +"1.0 = hour\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " +msgstr "" +"1.0 = hora\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_kanban_inherit +#, fuzzy +msgid "" +"
\n" +" Rounded: " +msgstr "" +"
\n" +" Redondeo: " + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_account_analytic_line +msgid "Analytic Line" +msgstr "Línea analítica" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__half_up +msgid "Closest" +msgstr "Más cercano" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__display_name +msgid "Display Name" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__down +msgid "Down" +msgstr "Abajo" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.hr_timesheet_view_task_form2_inherited_inherit +msgid "Duration (rounded)" +msgstr "Duración (redondeado)" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__id +msgid "ID" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "" +"If you activate the rounding of timesheet lines, only new entries will be " +"rounded (i.e. existing lines will not be rounded automatically)." +msgstr "" +"Si activa el redondeo de las líneas del parte de horas, solo se redondearán " +"las nuevas entradas (es decir, las líneas existentes no se redondearán " +"automáticamente)." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__no +msgid "No rounding" +msgstr "Sin redondeo" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_project_project +msgid "Project" +msgstr "Proyecto" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.view_account_analytic_line_form_inherit +msgid "Quantity Rounded" +msgstr "Cantidad redondeada" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__unit_amount_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_hr_timesheet_switch__unit_amount_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Quantity rounded" +msgstr "Cantidad redondeada" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea Pedido Venta" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.project_project_form_inherit +msgid "Time rounding" +msgstr "Redondeo de tiempo" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_factor +msgid "Timesheet rounding factor in percentage" +msgstr "Factor de redondeo del Parte de horas en porcentaje" + +#. module: sale_timesheet_rounded +#: model:ir.model.constraint,message:sale_timesheet_rounded.constraint_project_project_check_timesheet_rounding_factor +msgid "" +"Timesheet rounding factor should stay between 0 and 500, endpoints included." +msgstr "" +"El factor de redondeo del Parte de horas debe estar entre 0 y 500, incluidos " +"los puntos finales." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "Timesheet rounding method" +msgstr "Método de redondeo del Parte de horas" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "Timesheet rounding unit" +msgstr "Unidad de redondeo del Parte de horas" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Total quantity rounded" +msgstr "Cantidad total redondeada" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__up +msgid "Up" +msgstr "Arriba" diff --git a/sale_timesheet_rounded/i18n/it.po b/sale_timesheet_rounded/i18n/it.po new file mode 100644 index 000000000..326fa7df2 --- /dev/null +++ b/sale_timesheet_rounded/i18n/it.po @@ -0,0 +1,135 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_rounded +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-02-01 16:48+0000\n" +"Last-Translator: Francesco Foresti \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" +"X-Generator: Weblate 4.14.1\n" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "" +"1.0 = hour\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " +msgstr "" +"1.0 = ora\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_kanban_inherit +msgid "" +"
\n" +" Rounded: " +msgstr "" +"
\n" +" Arrotondato: " + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_account_analytic_line +msgid "Analytic Line" +msgstr "Riga analitica" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__half_up +msgid "Closest" +msgstr "Più vicino" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__down +msgid "Down" +msgstr "Giu" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.hr_timesheet_view_task_form2_inherited_inherit +msgid "Duration (rounded)" +msgstr "Durata (arrotondata)" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "" +"If you activate the rounding of timesheet lines, only new entries will be " +"rounded (i.e. existing lines will not be rounded automatically)." +msgstr "" +"Se si attiva l'arrotondamento delle righe del foglio ore, solo le nuove " +"registrazioni verranno arrotondate (i.e. le righe inserite non verranno " +"arrotondate automaticamente)." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__no +msgid "No rounding" +msgstr "Senza arrotondamento" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_project_project +msgid "Project" +msgstr "Progetto" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.view_account_analytic_line_form_inherit +msgid "Quantity Rounded" +msgstr "Quantità arrotondata" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__unit_amount_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Quantity rounded" +msgstr "Quantità arrotondata" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "Rounding Unit" +msgstr "Unità arrotondamento" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "Rounding method" +msgstr "Metodo arrotondamento" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.project_project_form_inherit +msgid "Time rounding" +msgstr "Arrotondamento orario" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_factor +msgid "Timesheet rounding factor in percentage" +msgstr "Fattore arrotondamento foglio ore in percentuale" + +#. module: sale_timesheet_rounded +#: model:ir.model.constraint,message:sale_timesheet_rounded.constraint_project_project_check_timesheet_rounding_factor +msgid "" +"Timesheet rounding factor should stay between 0 and 500, endpoints included." +msgstr "" +"il fattore di arrotondamento del foglio ore deve stare tra 0 e 500, estremi " +"inclusi." + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Total quantity rounded" +msgstr "Totale quantità arrotondata" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__up +msgid "Up" +msgstr "Su" diff --git a/sale_timesheet_rounded/i18n/nl.po b/sale_timesheet_rounded/i18n/nl.po new file mode 100644 index 000000000..23794b03d --- /dev/null +++ b/sale_timesheet_rounded/i18n/nl.po @@ -0,0 +1,155 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_rounded +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2021-04-22 17:47+0000\n" +"Last-Translator: Bosd \n" +"Language-Team: none\n" +"Language: nl\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 4.3.2\n" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "" +"1.0 = hour\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " +msgstr "" +"1.0 = uur\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_kanban_inherit +msgid "" +"
\n" +" Rounded: " +msgstr "" +"
\n" +" Afgerond: " + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_account_analytic_line +msgid "Analytic Line" +msgstr "Kostenplaatsregel" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__half_up +msgid "Closest" +msgstr "Ditchsbijzijnde" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__display_name +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Weergavenaam" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__down +msgid "Down" +msgstr "Omlaag" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.hr_timesheet_view_task_form2_inherited_inherit +msgid "Duration (rounded)" +msgstr "Tijdsduur (afgerond)" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__id +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line__id +msgid "ID" +msgstr "ID" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "" +"If you activate the rounding of timesheet lines, only new entries will be " +"rounded (i.e. existing lines will not be rounded automatically)." +msgstr "" +"Wanner het afronden van tijdlijst regels is geactiveerd, zullen enkel nieuwe " +"invoeren worden afgerond. (Bestaande regels zullen niet automatisch worden " +"afgerond)." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project____last_update +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "Laatst bijgewerkt op" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__no +msgid "No rounding" +msgstr "Geen afronding" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_project_project +msgid "Project" +msgstr "Project" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.view_account_analytic_line_form_inherit +msgid "Quantity Rounded" +msgstr "Aantal afgerond" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__unit_amount_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_hr_timesheet_switch__unit_amount_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Quantity rounded" +msgstr "Aantal afgerond" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_sale_order_line +msgid "Sales Order Line" +msgstr "Verkooporder regel" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.project_project_form_inherit +msgid "Time rounding" +msgstr "Tijdsafronding" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_factor +msgid "Timesheet rounding factor in percentage" +msgstr "Tijdlijst afrondingsfactor in percentage" + +#. module: sale_timesheet_rounded +#: model:ir.model.constraint,message:sale_timesheet_rounded.constraint_project_project_check_timesheet_rounding_factor +msgid "" +"Timesheet rounding factor should stay between 0 and 500, endpoints included." +msgstr "Tijdlijst afrondings factor zou moeten liggen tussen de 0 en 500." + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "Timesheet rounding method" +msgstr "Tijdlijst afrondingsmethode" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "Timesheet rounding unit" +msgstr "Tijdlijst afrondings eenheid" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Total quantity rounded" +msgstr "Totaal aantal afgerond" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__up +msgid "Up" +msgstr "Omhoog" diff --git a/sale_timesheet_rounded/i18n/sale_timesheet_rounded.pot b/sale_timesheet_rounded/i18n/sale_timesheet_rounded.pot new file mode 100644 index 000000000..050b2aa26 --- /dev/null +++ b/sale_timesheet_rounded/i18n/sale_timesheet_rounded.pot @@ -0,0 +1,120 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_rounded +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "" +"1.0 = hour\n" +" 0.25 = 15 min\n" +" 0.084 ~= 5 min\n" +" 0.017 ~= 1 min\n" +" " +msgstr "" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_kanban_inherit +msgid "" +"
\n" +" Rounded: " +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_account_analytic_line +msgid "Analytic Line" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__half_up +msgid "Closest" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__down +msgid "Down" +msgstr "" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.hr_timesheet_view_task_form2_inherited_inherit +msgid "Duration (rounded)" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,help:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "" +"If you activate the rounding of timesheet lines, only new entries will be " +"rounded (i.e. existing lines will not be rounded automatically)." +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__no +msgid "No rounding" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_project_project +msgid "Project" +msgstr "" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.view_account_analytic_line_form_inherit +msgid "Quantity Rounded" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_account_analytic_line__unit_amount_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Quantity rounded" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_unit +msgid "Rounding Unit" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_method +msgid "Rounding method" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model,name:sale_timesheet_rounded.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.project_project_form_inherit +msgid "Time rounding" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields,field_description:sale_timesheet_rounded.field_project_project__timesheet_rounding_factor +msgid "Timesheet rounding factor in percentage" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.constraint,message:sale_timesheet_rounded.constraint_project_project_check_timesheet_rounding_factor +msgid "" +"Timesheet rounding factor should stay between 0 and 500, endpoints included." +msgstr "" + +#. module: sale_timesheet_rounded +#: model_terms:ir.ui.view,arch_db:sale_timesheet_rounded.account_analytic_line_tree_inherit +msgid "Total quantity rounded" +msgstr "" + +#. module: sale_timesheet_rounded +#: model:ir.model.fields.selection,name:sale_timesheet_rounded.selection__project_project__timesheet_rounding_method__up +msgid "Up" +msgstr "" diff --git a/sale_timesheet_rounded/models/__init__.py b/sale_timesheet_rounded/models/__init__.py new file mode 100644 index 000000000..abed3dc64 --- /dev/null +++ b/sale_timesheet_rounded/models/__init__.py @@ -0,0 +1,4 @@ +from . import account_analytic_line +from . import project_project +from . import sale +from . import account_move diff --git a/sale_timesheet_rounded/models/account_analytic_line.py b/sale_timesheet_rounded/models/account_analytic_line.py new file mode 100644 index 000000000..cb0a07871 --- /dev/null +++ b/sale_timesheet_rounded/models/account_analytic_line.py @@ -0,0 +1,146 @@ +# Copyright 2019 Camptocamp SA +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.tools.float_utils import float_round + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + unit_amount_rounded = fields.Float( + string="Quantity rounded", + compute="_compute_unit_rounded", + store=True, + readonly=False, + copy=False, + ) + + @api.depends("timesheet_invoice_id.state") + def _compute_project_id(self): + field_rounded = self._fields["unit_amount_rounded"] + if self._context.get("timesheet_no_recompute", False): + self.env.remove_to_compute(field_rounded, self) + return super()._compute_project_id() + + @api.depends("project_id", "unit_amount") + def _compute_unit_rounded(self): + for record in self: + record.unit_amount_rounded = record._calc_unit_amount_rounded() + + def _calc_unit_amount_rounded(self): + self.ensure_one() + project_rounding = ( + self.project_id and self.project_id.timesheet_rounding_method != "NO" + ) + if project_rounding: + return self._calc_rounded_amount( + self.project_id.timesheet_rounding_unit, + self.project_id.timesheet_rounding_method, + self.project_id.timesheet_rounding_factor, + self.unit_amount, + ) + else: + return self.unit_amount + + @staticmethod + def _calc_rounded_amount(rounding_unit, rounding_method, factor, amount): + factor = factor / 100.0 + if rounding_unit: + unit_amount_rounded = float_round( + amount * factor, + precision_rounding=rounding_unit, + rounding_method=rounding_method, + ) + else: + unit_amount_rounded = amount * factor + return unit_amount_rounded + + #################################################### + # ORM Overrides + #################################################### + + @api.model + def _read_group( + self, + domain, + groupby=(), + aggregates=(), + having=(), + offset=0, + limit=None, + order=None, + ): + """Replace the value of unit_amount by unit_amount_rounded. + + When context key `timesheet_rounding` is True + we change the value of unit_amount with the rounded one. + This affects `sale_order_line._compute_delivered_quantity` + which in turns compute the delivered qty on SO line. + """ + ctx_ts_rounded = self.env.context.get("timesheet_rounding") + new_aggregates = list(aggregates) if aggregates else [] + if ctx_ts_rounded: + if ( + "unit_amount:sum" in aggregates + and "unit_amount_rounded:sum" not in aggregates + ): + # To add the unit_amount_rounded value on read_group + new_aggregates.append("unit_amount_rounded:sum") + res = super()._read_group( + domain=domain, + groupby=groupby, + aggregates=new_aggregates, + having=having, + offset=offset, + limit=limit, + order=order, + ) + + if ctx_ts_rounded: + update_aggregates = ( + "unit_amount:sum" in new_aggregates + and "unit_amount_rounded:sum" in new_aggregates + ) + if update_aggregates: + rec_ua_field_index = len(groupby) + new_aggregates.index( + "unit_amount:sum" + ) + rec_uar_field_index = len(groupby) + new_aggregates.index( + "unit_amount_rounded:sum" + ) + for rec_index, rec in enumerate(res): + rec_list = list(rec) + if rec[rec_uar_field_index]: + rec_list[rec_ua_field_index] = rec[rec_uar_field_index] + # .../addons/sale/models/sale_order_line.py#L737 + # Dealing with sale.order.line case which hardcode + # aggregates parameters, so we have to remove the excessive + if len(rec_list) > len(groupby) + len(aggregates): + del rec_list[rec_uar_field_index] + res[rec_index] = tuple(rec_list) + + return res + + def read(self, fields=None, load="_classic_read"): + """Replace the value of unit_amount by unit_amount_rounded. + + When context key `timesheet_rounding` is True + we change the value of unit_amount with the rounded one. + This affects `account_analytic_line._sale_determine_order_line`. + """ + ctx_ts_rounded = self.env.context.get("timesheet_rounding") + fields_local = list(fields) if fields else [] + read_unit_amount = "unit_amount" in fields_local or not fields_local + if ctx_ts_rounded and read_unit_amount and fields_local: + if "unit_amount_rounded" not in fields_local: + # To add the unit_amount_rounded value on read + fields_local.append("unit_amount_rounded") + res = super().read(fields=fields_local, load=load) + if ctx_ts_rounded and read_unit_amount: + # To set the unit_amount_rounded value instead of unit_amount + for rec in res: + if rec.get("unit_amount_rounded"): + rec["unit_amount"] = rec["unit_amount_rounded"] + return res diff --git a/sale_timesheet_rounded/models/account_move.py b/sale_timesheet_rounded/models/account_move.py new file mode 100644 index 000000000..4234b85da --- /dev/null +++ b/sale_timesheet_rounded/models/account_move.py @@ -0,0 +1,30 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _post(self, soft=True): + # We must avoid the recomputation of the unit amount rounded called by + # the compute_project_id (especially when project has not been changed) + return super(AccountMove, self.with_context(timesheet_no_recompute=True))._post( + soft=soft + ) + + def unlink(self): + return super( + AccountMove, self.with_context(timesheet_no_recompute=True) + ).unlink() + + def button_cancel(self): + return super( + AccountMove, self.with_context(timesheet_no_recompute=True) + ).button_cancel() + + def button_draft(self): + return super( + AccountMove, self.with_context(timesheet_no_recompute=True) + ).button_draft() diff --git a/sale_timesheet_rounded/models/project_project.py b/sale_timesheet_rounded/models/project_project.py new file mode 100644 index 000000000..bdfc366a7 --- /dev/null +++ b/sale_timesheet_rounded/models/project_project.py @@ -0,0 +1,44 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models + + +class ProjectProject(models.Model): + _inherit = "project.project" + + timesheet_rounding_unit = fields.Float( + string="Rounding Unit", + default=0.0, + help="""1.0 = hour + 0.25 = 15 min + 0.084 ~= 5 min + 0.017 ~= 1 min + """, + ) + timesheet_rounding_method = fields.Selection( + string="Rounding method", + selection=[ + ("NO", "No rounding"), + ("UP", "Up"), + ("HALF_UP", "Closest"), + ("DOWN", "Down"), + ], + default="NO", + required=True, + help="If you activate the rounding of timesheet lines, only new " + "entries will be rounded (i.e. existing lines will not be " + "rounded automatically).", + ) + timesheet_rounding_factor = fields.Float( + string="Timesheet rounding factor in percentage", default=100.0 + ) + + _sql_constraints = [ + ( + "check_timesheet_rounding_factor", + "CHECK(0 <= timesheet_rounding_factor " + "AND timesheet_rounding_factor <= 500)", + "Timesheet rounding factor should stay between 0 and 500," + " endpoints included.", + ) + ] diff --git a/sale_timesheet_rounded/models/sale.py b/sale_timesheet_rounded/models/sale.py new file mode 100644 index 000000000..3c887959d --- /dev/null +++ b/sale_timesheet_rounded/models/sale.py @@ -0,0 +1,24 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + + +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _get_delivered_quantity_by_analytic(self, additional_domain): + # If we land here is only because we are dealing w/ SO lines + # having `qty_delivered_method` equal to `analytic` or `timesheet`. + # The 1st case matches expenses lines the latter TS lines. + # Expenses are already discarded in our a.a.l. overrides + # so it's fine to set the ctx key here anyway. + return super( + SaleOrderLine, self.with_context(timesheet_rounding=True) + )._get_delivered_quantity_by_analytic(additional_domain) + + @api.depends("analytic_line_ids.unit_amount_rounded") + def _compute_qty_delivered(self): + """Adds the dependency on unit_amount_rounded.""" + return super()._compute_qty_delivered() diff --git a/sale_timesheet_rounded/pyproject.toml b/sale_timesheet_rounded/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/sale_timesheet_rounded/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_timesheet_rounded/readme/CONFIGURE.md b/sale_timesheet_rounded/readme/CONFIGURE.md new file mode 100644 index 000000000..084b8e6c5 --- /dev/null +++ b/sale_timesheet_rounded/readme/CONFIGURE.md @@ -0,0 +1,21 @@ +Go to a project and set the following fields according to your needs: + +- Timesheet rounding unit + +Defines the rounding unit. For instance, if you want to round to 1 hour, +you can set 1.0. If you want to round to 15 min set 0.25. + +- Timesheet rounding method + +Options: "No" (default), "Closest", "Up", "Down". + +Please refer to odoo.tools.float_utils.float_round to understand the +difference. + +- Timesheet rounding factor (percentage) + +When round unit is not defined you can round by a fixed %. + +When using both a unit and a factor, the factor will be applied first: + +> result = round(amount \* percentage, unit) diff --git a/sale_timesheet_rounded/readme/CONTRIBUTORS.md b/sale_timesheet_rounded/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..fee175af4 --- /dev/null +++ b/sale_timesheet_rounded/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- Simone Orsi \<\> +- Thomas Nowicki \<\> +- Akim Juillerat \<\> +- Foram Shah \<\> +- Phuc Kieu \<\> +- Son Ho \<\> diff --git a/sale_timesheet_rounded/readme/CREDITS.md b/sale_timesheet_rounded/readme/CREDITS.md new file mode 100644 index 000000000..05c7b9f57 --- /dev/null +++ b/sale_timesheet_rounded/readme/CREDITS.md @@ -0,0 +1,2 @@ +The migration of this sale_timesheet_rounded from 16.0 to 17.0 was +financially supported by Camptocamp diff --git a/sale_timesheet_rounded/readme/DESCRIPTION.md b/sale_timesheet_rounded/readme/DESCRIPTION.md new file mode 100644 index 000000000..69a183771 --- /dev/null +++ b/sale_timesheet_rounded/readme/DESCRIPTION.md @@ -0,0 +1,20 @@ +Round timesheet lines amounts in sales based on project' settings. + +A typical use case is: you work 5 minutes but you want to invoice 15 +minutes. + +With this module you can configure a rounding unit or factor on the +project and all the lines tracked on this project's tasks will show a +rounded amount. + +If you want you can override the value manually on each entry. + +The delivered quantity on the sale order line - and by consequence on +the invoice - will be computed using the rounded amount. Therefore, +expense lines and other non-timesheet lines will be updated with a +rounded amount that is equal to the amount. + +WARNING: This module cannot be used with timesheet_grid without further +adapation as an update of an existing timesheet line will NOT update the +rounded amount. To achieve this, you need to override adjust_grid +function to pass the force_compute context key. diff --git a/sale_timesheet_rounded/readme/ROADMAP.md b/sale_timesheet_rounded/readme/ROADMAP.md new file mode 100644 index 000000000..80e647989 --- /dev/null +++ b/sale_timesheet_rounded/readme/ROADMAP.md @@ -0,0 +1 @@ +- improve test coverage diff --git a/sale_timesheet_rounded/static/description/icon.png b/sale_timesheet_rounded/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/sale_timesheet_rounded/static/description/icon.png differ diff --git a/sale_timesheet_rounded/static/description/index.html b/sale_timesheet_rounded/static/description/index.html new file mode 100644 index 000000000..31175c260 --- /dev/null +++ b/sale_timesheet_rounded/static/description/index.html @@ -0,0 +1,475 @@ + + + + + +Sale Timesheet Rounded + + + +
+

Sale Timesheet Rounded

+ + +

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

+

Round timesheet lines amounts in sales based on project’ settings.

+

A typical use case is: you work 5 minutes but you want to invoice 15 +minutes.

+

With this module you can configure a rounding unit or factor on the +project and all the lines tracked on this project’s tasks will show a +rounded amount.

+

If you want you can override the value manually on each entry.

+

The delivered quantity on the sale order line - and by consequence on +the invoice - will be computed using the rounded amount. Therefore, +expense lines and other non-timesheet lines will be updated with a +rounded amount that is equal to the amount.

+

WARNING: This module cannot be used with timesheet_grid without further +adapation as an update of an existing timesheet line will NOT update the +rounded amount. To achieve this, you need to override adjust_grid +function to pass the force_compute context key.

+

Table of contents

+ +
+

Configuration

+

Go to a project and set the following fields according to your needs:

+
    +
  • Timesheet rounding unit
  • +
+

Defines the rounding unit. For instance, if you want to round to 1 hour, +you can set 1.0. If you want to round to 15 min set 0.25.

+
    +
  • Timesheet rounding method
  • +
+

Options: “No” (default), “Closest”, “Up”, “Down”.

+

Please refer to odoo.tools.float_utils.float_round to understand the +difference.

+
    +
  • Timesheet rounding factor (percentage)
  • +
+

When round unit is not defined you can round by a fixed %.

+

When using both a unit and a factor, the factor will be applied first:

+
+result = round(amount * percentage, unit)
+
+
+

Known issues / Roadmap

+
    +
  • improve test coverage
  • +
+
+
+

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

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this sale_timesheet_rounded from 16.0 to 17.0 was +financially supported by Camptocamp

+
+
+

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/timesheet project on GitHub.

+

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

+
+
+
+ + diff --git a/sale_timesheet_rounded/tests/__init__.py b/sale_timesheet_rounded/tests/__init__.py new file mode 100644 index 000000000..a82ca2ecc --- /dev/null +++ b/sale_timesheet_rounded/tests/__init__.py @@ -0,0 +1 @@ +from . import test_rounded diff --git a/sale_timesheet_rounded/tests/test_rounded.py b/sale_timesheet_rounded/tests/test_rounded.py new file mode 100644 index 000000000..cdcdff4f9 --- /dev/null +++ b/sale_timesheet_rounded/tests/test_rounded.py @@ -0,0 +1,338 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import odoo +from odoo import fields + +from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet + + +@odoo.tests.tagged("post_install", "-at_install") +class TestRounded(TestCommonSaleTimesheet): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.sale_order = cls.env["sale.order"].create( + { + "analytic_account_id": cls.project_global.analytic_account_id.id, + "partner_id": cls.partner_a.id, + "partner_invoice_id": cls.partner_a.id, + "partner_shipping_id": cls.partner_a.id, + } + ) + cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order.id, + "name": cls.product_delivery_timesheet2.name, + "product_id": cls.product_delivery_timesheet2.id, + "product_uom_qty": 1, + "product_uom": cls.product_delivery_timesheet2.uom_id.id, + "price_unit": cls.product_delivery_timesheet2.list_price, + } + ) + cls.sale_order.action_confirm() + cls.project_global.write( + { + "timesheet_rounding_unit": 0.25, + "timesheet_rounding_method": "UP", + "timesheet_rounding_factor": 200, + } + ) + cls.product_expense = cls.env["product.product"].create( + { + "name": "Service delivered, EXPENSE", + "expense_policy": "cost", + "standard_price": 30, + "list_price": 90, + "type": "service", + "invoice_policy": "order", + "uom_id": cls.product_delivery_timesheet2.uom_id.id, + "uom_po_id": cls.product_delivery_timesheet2.uom_id.id, + } + ) + cls.analytic_plan = cls.env["account.analytic.plan"].create( + { + "name": "Plan sale timesheet", + } + ) + cls.avg_analytic_account = cls.env["account.analytic.account"].create( + { + "name": "AVG account", + "plan_id": cls.analytic_plan.id, + } + ) + + def create_analytic_line(self, **kw): + task = self.sale_order.tasks_ids[0] + values = { + "project_id": self.project_global.id, + "task_id": task.id, + "name": "Rounded test line", + "date": fields.Date.today(), + "unit_amount": 0, + "product_id": self.product_delivery_timesheet2.id, + "employee_id": self.employee_user.id, + } + values.update(kw) + return self.env["account.analytic.line"].create(values) + + def test_analytic_line_init_no_rounding(self): + lines = self.env["account.analytic.line"].search([]) + for line in lines: + self.assertEqual(line.unit_amount_rounded, line.unit_amount) + + def test_analytic_line_create_no_rounding(self): + self.project_global.write({"timesheet_rounding_method": "NO"}) + # no rounding enabled + line = self.create_analytic_line(unit_amount=1) + self.assertEqual(line.unit_amount, 1.0) + self.assertEqual(line.unit_amount_rounded, line.unit_amount) + + def test_analytic_line_create(self): + line = self.create_analytic_line(unit_amount=1) + self.assertEqual(line.unit_amount_rounded, 2.0) + line = self.create_analytic_line(unit_amount=1, unit_amount_rounded=0) + self.assertEqual(line.unit_amount_rounded, 0.0) + + def test_analytic_line_create_and_update_amount_rounded(self): + line = self.create_analytic_line(unit_amount=2) + self.assertEqual(line.unit_amount_rounded, 4.0) + line.write({"unit_amount_rounded": 5.0}) + self.assertEqual(line.unit_amount_rounded, 5.0) + line.write({"unit_amount_rounded": 0.0}) + self.assertEqual(line.unit_amount_rounded, 0.0) + + def test_analytic_line_create_and_update_amount(self): + line = self.create_analytic_line(unit_amount=2) + self.assertEqual(line.unit_amount_rounded, 4.0) + line.unit_amount = 5.0 + self.assertEqual(line.unit_amount_rounded, 10.0) + + def test_analytic_line_read_group_override(self): + # Test of the read group with an without timesheet_rounding context + # without context the unit_amount should be the initial + # with the context the value of unit_amount should be replaced by the + # unit_amount_rounded + line = self.env["account.analytic.line"] + self.create_analytic_line(unit_amount=1) + domain = [("project_id", "=", self.project_global.id)] + groupby = ["product_uom_id", "so_line"] + aggregates = ["unit_amount:sum"] + + data_ctx_f = line._read_group( + domain, + groupby, + aggregates, + ) + self.assertEqual( + data_ctx_f[0][len(groupby) + aggregates.index("unit_amount:sum")], 1.0 + ) + + data_ctx_t = line.with_context(timesheet_rounding=True)._read_group( + domain, + groupby, + aggregates, + ) + self.assertEqual( + data_ctx_t[0][len(groupby) + aggregates.index("unit_amount:sum")], 2.0 + ) + + self.create_analytic_line(unit_amount=1.1) + data_ctx_f = line.with_context(timesheet_rounding=False)._read_group( + domain, + groupby, + aggregates, + ) + self.assertEqual( + data_ctx_f[0][len(groupby) + aggregates.index("unit_amount:sum")], 2.1 + ) + + data_ctx_f = line.with_context(timesheet_rounding=True)._read_group( + domain, + groupby, + aggregates, + ) + self.assertEqual( + data_ctx_f[0][len(groupby) + aggregates.index("unit_amount:sum")], 4.25 + ) + + def test_analytic_line_read_override(self): + # Cases for not rounding: + # * not linked to project -> no impact + # * is an expense -> no impact + # * ctx key for rounding is set to false -> no impact + # In all the other cases we check that unit amount is rounded. + load = "_classic_read" + fields = None + + # context = False + project_id - product_expense + line = self.create_analytic_line(unit_amount=1) + unit_amount_ret = line.read(fields, load)[0]["unit_amount"] + self.assertEqual(unit_amount_ret, 1) + + # context = True + project_id + product_expense + line = self.create_analytic_line( + unit_amount=1, product_id=self.product_expense.id + ) + unit_amount_ret = line.with_context(timesheet_rounding=True).read(fields, load)[ + 0 + ]["unit_amount"] + self.assertEqual(unit_amount_ret, 2) + + # context = True + project_id - product_expense + line = self.create_analytic_line(unit_amount=1) + unit_amount_ret = line.with_context(timesheet_rounding=True).read(fields, load)[ + 0 + ]["unit_amount"] + self.assertEqual(unit_amount_ret, 2) + + def test_sale_order_qty_1(self): + # amount=1 -> should be rounded to 2 by the invoicing_factor + self.create_analytic_line(unit_amount=1) + self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 2.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 2.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) + + def test_sale_order_qty_2(self): + # force amount_rounded=4 + self.create_analytic_line(unit_amount=1, unit_amount_rounded=4) + self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 4.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 4.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) + + def test_sale_order_qty_3(self): + # amount=0.9 + # should be rounded to 2 by the invoicing_factor with the project + # timesheet_rounding_unit: 0.25 + # timesheet_rounding_method: 'UP' + # timesheet_rounding_factor: 200 + self.create_analytic_line(unit_amount=0.9) + self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 2.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 2.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) + + def test_sale_order_qty_4(self): + # amount=0.9 + # should be rounded to 2 by the invoicing_factor with the project + # timesheet_rounding_unit: 0.25 + # timesheet_rounding_method: 'UP' + # timesheet_rounding_factor: 200 + self.project_global.timesheet_rounding_factor = 400 + self.create_analytic_line(unit_amount=1.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 4.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 4.0) + self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) + + def test_calc_rounded_amount_method(self): + aal = self.env["account.analytic.line"] + rounding_unit = 0.25 + rounding_method = "UP" + factor = 200 + amount = 1 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 + ) + + rounding_unit = 0.0 + rounding_method = "UP" + factor = 200 + amount = 1 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 + ) + + rounding_unit = 0.25 + rounding_method = "UP" + factor = 100 + amount = 1.0 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 1 + ) + + rounding_unit = 0.25 + rounding_method = "UP" + factor = 200 + amount = 0.9 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 + ) + + rounding_unit = 1.0 + rounding_method = "UP" + factor = 200 + amount = 0.6 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 + ) + + rounding_unit = 0.25 + rounding_method = "HALF_UP" + factor = 200 + amount = 1.01 + self.assertEqual( + aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 + ) + + def test_post_invoice_with_rounded_amount_unchanged(self): + """Posting an invoice MUST NOT recompute rounded amount unit. + - invoicing the SO should not recompute and update the + unit_amount_rounded + - the invoiced qty should be the same as the aal.unit_amount_rounded + """ + unit_amount_rounded = 111 + analytic_line = self.create_analytic_line(unit_amount=10) + analytic_line.unit_amount_rounded = unit_amount_rounded + account_move = self.sale_order._create_invoices() + prd_ts_id = self.product_delivery_timesheet2 + account_move._post() + # the unit_amount_rounded is not changed + self.assertEqual(analytic_line.unit_amount_rounded, unit_amount_rounded) + # the invoiced qty remains the same + inv_line = account_move.line_ids.filtered( + lambda line: line.product_id == prd_ts_id + ) + self.assertEqual(inv_line.quantity, unit_amount_rounded) + + def test_draft_invoice_with_rounded_amount_unchanged(self): + """Drafting an invoice MUST NOT recompute rounded amount unit. + - invoicing the SO should not recompute and update the + unit_amount_rounded + - the invoiced qty should be the same as the aal.unit_amount_rounded + """ + unit_amount_rounded = 0.12 + analytic_line = self.create_analytic_line(unit_amount=10) + analytic_line.unit_amount_rounded = unit_amount_rounded + account_move = self.sale_order._create_invoices() + account_move.action_post() + prd_ts_id = self.product_delivery_timesheet2 + account_move.button_draft() + # the unit_amount_rounded is not changed + self.assertEqual(analytic_line.unit_amount_rounded, unit_amount_rounded) + # the invoiced qty remains the same + inv_line = account_move.line_ids.filtered( + lambda line: line.product_id == prd_ts_id + ) + self.assertEqual(inv_line.quantity, unit_amount_rounded) + + def test_cancel_invoice_with_rounded_amount_unchanged(self): + """Cancelling an invoice MUST NOT recompute rounded amount unit. + - invoicing the SO should not recompute and update the + unit_amount_rounded + - the invoiced qty should be the same as the aal.unit_amount_rounded + """ + unit_amount_rounded_total = 15 + analytic_line_1 = self.create_analytic_line(unit_amount=10) + analytic_line_2 = self.create_analytic_line(unit_amount=10) + analytic_line_1.unit_amount_rounded = unit_amount_rounded_total + analytic_line_2.unit_amount_rounded = 0 + account_move = self.sale_order._create_invoices() + prd_ts_id = self.product_delivery_timesheet2 + account_move.button_cancel() + # the unit_amount_rounded is not changed + self.assertEqual(analytic_line_1.unit_amount_rounded, unit_amount_rounded_total) + self.assertEqual(analytic_line_2.unit_amount_rounded, 0) + # the invoiced qty remains the same + inv_line = account_move.line_ids.filtered( + lambda line: line.product_id == prd_ts_id + ) + self.assertEqual(inv_line.quantity, unit_amount_rounded_total) diff --git a/sale_timesheet_rounded/views/account_analytic_line.xml b/sale_timesheet_rounded/views/account_analytic_line.xml new file mode 100644 index 000000000..3cf6a11dc --- /dev/null +++ b/sale_timesheet_rounded/views/account_analytic_line.xml @@ -0,0 +1,45 @@ + + + + account.analytic.line.form.inherit + account.analytic.line + + + + + + + + + account.analytic.line.kanban.inherit + account.analytic.line + + + +
+ Rounded: + +
+
+
+ + account.analytic.line.tree.inherit + account.analytic.line + + + + + + + +
diff --git a/sale_timesheet_rounded/views/project_project.xml b/sale_timesheet_rounded/views/project_project.xml new file mode 100644 index 000000000..630bf77ae --- /dev/null +++ b/sale_timesheet_rounded/views/project_project.xml @@ -0,0 +1,25 @@ + + + + project.project.form.inherit + project.project + + + + + + + + + + + + + + diff --git a/sale_timesheet_rounded/views/project_task.xml b/sale_timesheet_rounded/views/project_task.xml new file mode 100644 index 000000000..d6561e815 --- /dev/null +++ b/sale_timesheet_rounded/views/project_task.xml @@ -0,0 +1,18 @@ + + + + hr.timesheet.view.task.form2.inherited.inherit + project.task + + + + + + + + diff --git a/sale_timesheet_rounded/wizard/__init__.py b/sale_timesheet_rounded/wizard/__init__.py new file mode 100644 index 000000000..fca48c3d0 --- /dev/null +++ b/sale_timesheet_rounded/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_make_invoice_advance diff --git a/sale_timesheet_rounded/wizard/sale_make_invoice_advance.py b/sale_timesheet_rounded/wizard/sale_make_invoice_advance.py new file mode 100644 index 000000000..a0df16c9e --- /dev/null +++ b/sale_timesheet_rounded/wizard/sale_make_invoice_advance.py @@ -0,0 +1,20 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models + + +class SaleAdvancePaymentInv(models.TransientModel): + _inherit = "sale.advance.payment.inv" + + def create_invoices(self): + """Override method from sale/wizard/sale_make_invoice_advance.py + + When the user want to invoice the timesheets to the SO + up to a specific period then we need to recompute the + qty_to_invoice for each product_id in sale.order.line, + before creating the invoice. + """ + return super( + SaleAdvancePaymentInv, self.with_context(timesheet_no_recompute=True) + ).create_invoices()