diff --git a/fieldservice_route/README.rst b/fieldservice_route/README.rst new file mode 100644 index 0000000000..93cb266660 --- /dev/null +++ b/fieldservice_route/README.rst @@ -0,0 +1,128 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=================== +Field Service Route +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:03bce8598c39527207e72e5dc268178416e8a99832453d1abb1860803b810eec + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Ffield--service-lightgray.png?logo=github + :target: https://github.com/OCA/field-service/tree/19.0/fieldservice_route + :alt: OCA/field-service +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/field-service-19-0/field-service-19-0-fieldservice_route + :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/field-service&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +A Field Service route is the sorted list of orders that one +worker/vehicle will perform during a day. + +This module allows you to manage your Field Service routes: when +assigning an order to a worker and scheduling it, a route is +automatically created for that worker and day. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To use this module, you need to: + +- Go to Field Service > Configuration > Stages +- Define the workflow of your routes by creating new stages whose type + is "Route" +- Go to Field Service > Master Data > Routes +- Create your routes by setting their name, worker, days, and capacity + +Usage +===== + +To use this module, you need to: + +- Go to Field Service +- Create or select an order +- Assign it to a worker and schedule it +- Go to Field Service > Dashboard > Day Routes. A new record has been + created. + +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 +------- + +* Gray Matter Logic + +Contributors +------------ + +- `Gray Matter Logic `__: + + - Maxime Chambreuil + +- Nikul Chaudhary +- Freni Patel +- `Tecnativa `__: + + - Víctor Martínez + +Other credits +------------- + + + +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. + +.. |maintainer-max3903| image:: https://github.com/max3903.png?size=40px + :target: https://github.com/max3903 + :alt: max3903 + +Current `maintainer `__: + +|maintainer-max3903| + +This module is part of the `OCA/field-service `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fieldservice_route/__init__.py b/fieldservice_route/__init__.py new file mode 100644 index 0000000000..69f7babdfb --- /dev/null +++ b/fieldservice_route/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/fieldservice_route/__manifest__.py b/fieldservice_route/__manifest__.py new file mode 100644 index 0000000000..b233de0389 --- /dev/null +++ b/fieldservice_route/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Field Service Route", + "summary": "Organize the routes of each day.", + "version": "19.0.1.0.0", + "category": "Field Service", + "license": "AGPL-3", + "author": "Gray Matter Logic, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/field-service", + "depends": ["fieldservice"], + "data": [ + "data/ir_sequence.xml", + "data/fsm_route_day_data.xml", + "data/fsm_stage_data.xml", + "security/ir_rule.xml", + "security/ir.model.access.csv", + "views/fsm_route_day.xml", + "views/fsm_route.xml", + "views/fsm_location.xml", + "views/fsm_route_dayroute.xml", + "views/fsm_order.xml", + "views/menu.xml", + ], + "demo": [ + "demo/fsm_route.xml", + "demo/fsm_location.xml", + "demo/fsm_route_dayroute.xml", + "demo/fsm_order.xml", + ], + "development_status": "Beta", + "maintainers": ["max3903"], +} diff --git a/fieldservice_route/data/fsm_route_day_data.xml b/fieldservice_route/data/fsm_route_day_data.xml new file mode 100644 index 0000000000..f7b9580dbd --- /dev/null +++ b/fieldservice_route/data/fsm_route_day_data.xml @@ -0,0 +1,29 @@ + + + Monday + + + + Tuesday + + + + Wednesday + + + + Thursday + + + + Friday + + + + Saturday + + + + Sunday + + diff --git a/fieldservice_route/data/fsm_stage_data.xml b/fieldservice_route/data/fsm_stage_data.xml new file mode 100644 index 0000000000..6ddb80ecca --- /dev/null +++ b/fieldservice_route/data/fsm_stage_data.xml @@ -0,0 +1,17 @@ + + + New + 10 + True + route + #ECF0F1 + + + + Closed + 20 + True + route + #7F8C8D + + diff --git a/fieldservice_route/data/ir_sequence.xml b/fieldservice_route/data/ir_sequence.xml new file mode 100644 index 0000000000..737487832f --- /dev/null +++ b/fieldservice_route/data/ir_sequence.xml @@ -0,0 +1,10 @@ + + + + FSM Day Route + fsm.route.dayroute + SR + 3 + + + diff --git a/fieldservice_route/demo/fsm_location.xml b/fieldservice_route/demo/fsm_location.xml new file mode 100644 index 0000000000..d35dd181e9 --- /dev/null +++ b/fieldservice_route/demo/fsm_location.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/fieldservice_route/demo/fsm_order.xml b/fieldservice_route/demo/fsm_order.xml new file mode 100644 index 0000000000..c80890d09d --- /dev/null +++ b/fieldservice_route/demo/fsm_order.xml @@ -0,0 +1,23 @@ + + + + + 2026-06-30 08:00:00 + + North route demo order 1 + + + + + 2026-06-30 10:00:00 + + North route demo order 2 + + + + + 2026-06-30 09:00:00 + + South route demo order 1 + + diff --git a/fieldservice_route/demo/fsm_route.xml b/fieldservice_route/demo/fsm_route.xml new file mode 100644 index 0000000000..89baae3cb1 --- /dev/null +++ b/fieldservice_route/demo/fsm_route.xml @@ -0,0 +1,32 @@ + + + North District Route + + 10 + + + + South District Route + + 5 + + + diff --git a/fieldservice_route/demo/fsm_route_dayroute.xml b/fieldservice_route/demo/fsm_route_dayroute.xml new file mode 100644 index 0000000000..d7054a0ffa --- /dev/null +++ b/fieldservice_route/demo/fsm_route_dayroute.xml @@ -0,0 +1,14 @@ + + + DR-NORTH-063026 + + 2026-06-30 + + + + DR-SOUTH-063026 + + 2026-06-30 + + + diff --git a/fieldservice_route/i18n/es.po b/fieldservice_route/i18n/es.po new file mode 100644 index 0000000000..00912d87a3 --- /dev/null +++ b/fieldservice_route/i18n/es.po @@ -0,0 +1,379 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-10-06 08:15+0000\n" +"PO-Revision-Date: 2026-04-12 12:43+0000\n" +"Last-Translator: Ed-Spain \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Añadir Rutas del Día aquí." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Añadir Rutas aquí." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Asignado a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacidad Disponible" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Cancelar pedido" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacidad" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "Cerrado" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completar pedido" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crear una Ruta del Día." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Fecha" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Ruta del Día" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Rutas del Día" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Reporte de las Rutas del Día" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Días" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Ubicación Final" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Ubicación de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Pedido de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Ubicación del Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Pedido de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Etapa de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Viernes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Última Ubicación" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Última Actualización en" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tiempo Máximo Permitido (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Máximo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacidad Máxima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Pedidos Máximos" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Número máximo de pedidos por ruta del día." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "Número máximo de pedidos que pueden ser añadidos a estar ruta del día." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Lunes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nombre" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Nuevo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Número de Pedidos" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Pedidos" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Persona" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Tiempo de Inicio Planeado" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planificación" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posicionamiento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Ruta del Día" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Días de Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rutas" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sábado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Etapa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Ubicación Inicial" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domingo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Equipo" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "" +"La ruta del día está excediendo el número máximo de pedidos de la ruta." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "¡La ruta %(route_name)s no se realiza el %(name)s!" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Jueves" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tiempo antes de horas extras (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Temporizador" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Martes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Miércoles" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Debe crear un Equipo FSM primero." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "pedidos por ruta del día" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" diff --git a/fieldservice_route/i18n/es_AR.po b/fieldservice_route/i18n/es_AR.po new file mode 100644 index 0000000000..d0ec363344 --- /dev/null +++ b/fieldservice_route/i18n/es_AR.po @@ -0,0 +1,385 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-06-25 18:09+0000\n" +"Last-Translator: Ignacio Buioli \n" +"Language-Team: none\n" +"Language: es_AR\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.17\n" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Añada Rutas del Día acá." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Añada Rutas acá." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Asignado a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacidad Disponible" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Cancelar pedido" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacidad" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completar pedido" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crear una Ruta del Día." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Fecha" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Ruta del Día" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Rutas del Día" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Reporte de las Rutas del Día" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Días" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Ubicación Final" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Ubicación del Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Pedido de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Ruta del Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Ruta del Día del Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Etapa de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Viernes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Última Ubicación" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Última Actualización en" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tiempo Máximo Permitido (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Máximo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacidad Máxima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Pedidos Máximos" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Número máximo de pedidos por ruta del día." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "Número máximo de pedidos que pueden ser añadidos a estar ruta del día." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Lunes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nombre" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Nuevo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Número de Pedidos" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Pedidos" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Persona" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Tiempo de Inicio Planeado" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planificación" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posicionamiento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Ruta del Día" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Días de Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rutas" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sábado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Etapa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Ubicación Inicial" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domingo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Equipo" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "" +"La ruta del día está excediendo el número máximo de pedidos de la ruta." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "¡La ruta %(route_name)s no se ejecuta en %(name)s!" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Jueves" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tiempo antes de horas extras (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Temporalizador" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Martes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Miércoles" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Debe crear un Equipo FSM primero." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "pedidos por ruta del día" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" + +#~ msgid "Order" +#~ msgstr "Pedido" + +#, python-format +#~ msgid "The route %s does not run on %s!" +#~ msgstr "¡La ruta %s no se realiza el %s!" diff --git a/fieldservice_route/i18n/es_CL.po b/fieldservice_route/i18n/es_CL.po new file mode 100644 index 0000000000..31bef754c5 --- /dev/null +++ b/fieldservice_route/i18n/es_CL.po @@ -0,0 +1,396 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-06-07 05:19+0000\n" +"Last-Translator: Nelson Ramírez Sánchez \n" +"Language-Team: none\n" +"Language: es_CL\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: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Agregue Rutas de Día aquí." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Agregar rutas aquí." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Asignado a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacidad Disponible" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Cancelar orden" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacidad" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completar orden" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crea un día de ruta." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Fecha" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Ruta del día" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Rutas de día" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Informe de rutas diarias" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Días" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Nombre para mostrar" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Ubicación final" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Ubicación del servicio de campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Orden de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Ruta de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Ruta de servicio de campo Dayroute" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Etapa de Servicio de Campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Viernes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Última ubicación" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Ultima actualización en" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitud" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tiempo máximo permitido (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Máximo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacidad Máxima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Pedidos Máximos" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Número máximo de pedidos por ruta diaria" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "Número máximo de pedidos que se pueden agregar a esta ruta de día." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Lunes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nombre" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Nuevo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Numero de Ordenes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Pedidos" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Persona" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Hora de Inicio Planificada" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planificación" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posicionamiento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Día de la Ruta" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Días de Ruta" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rutas" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sábado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Etapa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Lugar de Inicio" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domingo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Equipo" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "La ruta del día excede el número máximo de pedidos de la ruta." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Jueves" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tiempo antes de las horas extra (en horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Sincronización" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Martes" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Miércoles" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Primero debe crear un equipo FSM." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "pedidos por día ruta" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" + +#~ msgid "Equipment" +#~ msgstr "Equipamento" + +#~ msgid "Location" +#~ msgstr "Ubicación" + +#~ msgid "Order" +#~ msgstr "Orden" + +#~ msgid "Territory" +#~ msgstr "Territorio" + +#, python-format +#~ msgid "The route %s does not run on %s!" +#~ msgstr "La ruta %s no corre en %s!" + +#~ msgid "Worker" +#~ msgstr "Trabajador" diff --git a/fieldservice_route/i18n/fieldservice_route.pot b/fieldservice_route/i18n/fieldservice_route.pot new file mode 100644 index 0000000000..8fee4d41d0 --- /dev/null +++ b/fieldservice_route/i18n/fieldservice_route.pot @@ -0,0 +1,371 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.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: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "" diff --git a/fieldservice_route/i18n/it.po b/fieldservice_route/i18n/it.po new file mode 100644 index 0000000000..58d6ce0fc1 --- /dev/null +++ b/fieldservice_route/i18n/it.po @@ -0,0 +1,380 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-12-16 12:43+0000\n" +"Last-Translator: mymage \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 5.10.4\n" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Aggiungere qui percorsi giornalieri." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Aggiungere qui i percorsi." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Assegnato a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacità disponibile" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Annulla ordine" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacità" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "Chiusa" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completa ordine" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crea un percorso giornaliero." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Data" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Percorso giornaliero" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Percorsi giornalieri" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Resoconto percorsi giornalieri" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Giorni" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Destinazione finale" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Località assistenza sul campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Ordine assistenza sul campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Percorso assistenza sul campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Percorso giornaliero assistenza sul campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Fase assistenza sul campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Venerdì" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Ultima località" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitudine" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitudine" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tempo massimo consentito (ore)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Massimo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacità massima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Massimo ordini" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Numero massimo di ordini per percorso giornaliero." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "" +"Numero massimo di ordini che possono essere aggiunti a questo percorso " +"giornaliero." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Lunedì" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nome" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Nuovo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Numero di ordini" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Ordini" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Persona" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Ora inizio pianificata" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Pianificazione" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posizionamento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Percorso" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Percorso giornaliero" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Percorsi giornalieri" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Percorsi" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sabato" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Fase" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Località di partenza" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domenica" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Squadra" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "" +"Il percorso giornaliero supera il numero massimo di ordini per il percorso." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "Il percorso %(route_name)s non passa per %(name)s!" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Giovedì" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tempo prima dello straordinario (in ore)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Tempistica" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Martedì" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Mercoledì" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Prima serve creare una squadra FSM." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "ordini per percorso giornaliero" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/fieldservice_route/i18n/pt_BR.po b/fieldservice_route/i18n/pt_BR.po new file mode 100644 index 0000000000..dfa942877c --- /dev/null +++ b/fieldservice_route/i18n/pt_BR.po @@ -0,0 +1,398 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-06-22 20:08+0000\n" +"Last-Translator: Adriano Prado \n" +"Language-Team: none\n" +"Language: pt_BR\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.17\n" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Adicione Rotas do Dia aqui." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Adicione Rotas aqui." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Atribuído a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacidade Disponível" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Cancelar Ordem" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacidade" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completar Ordem" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crie Rota do Dia." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Data" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Rota do Dia" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Rotas do Dia" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Relatório de Rotas do Dia" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Dias" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Nome Exibido" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Localização Final" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Local do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Ordem de Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Rota do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Rota do Serviço de Campo do Dia" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Estágio do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Sexta" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Última Localização" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Última Atualização por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Última Atualização em" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitude" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitude" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tempo Máximo Permitido (em horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Máximo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacidade Máxima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Máximo de Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Número máximo de ordens por rota diária." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "" +"Números máximos de ordens que podem ser adicionados a esta rota no dia." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Segunda" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nome" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Novo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Número de Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Pessoa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Horário de Início Planejado" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planejamento" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posicionamento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Rota" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Rota do Dia" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Dias de Rota" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rotas" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sábado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Estágio" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Localização Inical" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domingo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Equipe" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "A rota do dia está excedendo o número máximo de ordens da rota." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "A rota %(route_name)s não funciona em %(name)s!" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Quinta" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tempo antes da hora extra (em horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Cronometragem" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Terça" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Quarta" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "" +"Primeiro você deve criar uma equipe para o FSM (Field Services Management)." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "ordens por dia de rota" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificação Feita em" + +#~ msgid "Equipment" +#~ msgstr "Equipamento" + +#~ msgid "Location" +#~ msgstr "Localização" + +#~ msgid "Order" +#~ msgstr "Ordem" + +#~ msgid "Territory" +#~ msgstr "Território" + +#, python-format +#~ msgid "The route %s does not run on %s!" +#~ msgstr "A rota %s não funciona em %s!" + +#~ msgid "Worker" +#~ msgstr "Profissional" diff --git a/fieldservice_route/i18n/pt_PT.po b/fieldservice_route/i18n/pt_PT.po new file mode 100644 index 0000000000..70683da5a9 --- /dev/null +++ b/fieldservice_route/i18n/pt_PT.po @@ -0,0 +1,396 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-07-21 10:19+0000\n" +"Last-Translator: Daniel Reis \n" +"Language-Team: none\n" +"Language: pt_PT\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: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Adicione Rotas do Dia aqui." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Adicione Rotas aqui." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Atribuído a" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Capacidade Disponível" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Cancelar ordem" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Capacidade" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Completar Ordem" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Crie um Dia de Rota." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Data" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Rota Dia" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Rotas Dia" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Relatório de Rotas Dia" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Relatório de Rotas Diurnas" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Nome Apresentado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Localização Final" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Local do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Ordem de Serviço" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Rota do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Rota Dia do Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Etapa de Serviço de Campo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Sexta" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Última Localização" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Última Atualização por" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Última Atualização em" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Latitude" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Longitude" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "Tempo Máximo Permitido (em horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Máximo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Capacidade Máxima" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Máximo de Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Número máximo de ordens por rota diária." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "Números máximos de ordens que podem ser adicionados a esta rota dia." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Segunda" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Nome" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Novo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Número de Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Ordens" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Pessoa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Hora de Início Planeada" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planeamento" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Posicionamento" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Rota" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Rota Dia" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Dias da Rota" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rotas" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Sábado" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Etapa" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Localização Inical" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Domingo" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Equipa" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "A rota do dia excede o número máximo de ordens para a rota." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Quinta" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Tempo antes de horas extra (em horas)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Temporização" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Terça" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tipo" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Quarta" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Antes deve criar uma Equipa FSM." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "ordens por rota dia" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificação em" + +#~ msgid "Equipment" +#~ msgstr "Equipamento" + +#~ msgid "Location" +#~ msgstr "Localização" + +#~ msgid "Order" +#~ msgstr "Ordem" + +#~ msgid "Territory" +#~ msgstr "Território" + +#, python-format +#~ msgid "The route %s does not run on %s!" +#~ msgstr "A rota %s não funciona em %s!" + +#~ msgid "Worker" +#~ msgstr "Trabalhador" diff --git a/fieldservice_route/i18n/tr.po b/fieldservice_route/i18n/tr.po new file mode 100644 index 0000000000..c7a752aff5 --- /dev/null +++ b/fieldservice_route/i18n/tr.po @@ -0,0 +1,384 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fieldservice_route +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-09-22 16:42+0000\n" +"Last-Translator: Tamer Sezgin \n" +"Language-Team: none\n" +"Language: tr\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.10.4\n" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute +msgid "Add Day Routes here." +msgstr "Buraya Günlük Rotaları ekleyin." + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route +msgid "Add Routes here." +msgstr "Rotaları buraya ekleyin." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__person_id +msgid "Assigned To" +msgstr "Atanan" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_remaining +msgid "Available Capacity" +msgstr "Mevcut Kapasite" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Cancel order" +msgstr "Siparişi İptal Et" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Capacity" +msgstr "Kapasite" + +#. module: fieldservice_route +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_close +msgid "Closed" +msgstr "" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Complete order" +msgstr "Siparişi Tamamla" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_day +msgid "Create a Route Day." +msgstr "Bir Rota Günü Oluşturun." + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_uid +msgid "Created by" +msgstr "Oluşturan" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__create_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__create_date +msgid "Created on" +msgstr "Oluşturulma" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date +msgid "Date" +msgstr "Tarih" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__dayroute_id +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Day Route" +msgstr "Gün Rotası" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_dash +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_dayroute_report +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_dash_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_dayroute_report +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_calendar +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_graph_view +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_pivot_view +msgid "Day Routes" +msgstr "Gün Rotaları" + +#. module: fieldservice_route +#: model_terms:ir.actions.act_window,help:fieldservice_route.action_fsm_route_dayroute_report +msgid "Day Routes Report" +msgstr "Gün Rota Raporları" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__day_ids +msgid "Days" +msgstr "Gün" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__display_name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__display_name +msgid "Display Name" +msgstr "Görünüm Adı" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__end_location_id +msgid "End Location" +msgstr "Bitiş Konumu" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_location +msgid "Field Service Location" +msgstr "Saha Servis Konumu" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_order +msgid "Field Service Order" +msgstr "Saha Servis Siparişi" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route +msgid "Field Service Route" +msgstr "Saha Hizmet Rotası" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_route_dayroute +msgid "Field Service Route Dayroute" +msgstr "Saha Servis Günlük Rotası" + +#. module: fieldservice_route +#: model:ir.model,name:fieldservice_route.model_fsm_stage +msgid "Field Service Stage" +msgstr "Saha Servis Aşaması" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__friday +msgid "Friday" +msgstr "Cuma" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__id +msgid "ID" +msgstr "ID" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__last_location_id +msgid "Last Location" +msgstr "Son Konum" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_uid +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_uid +msgid "Last Updated by" +msgstr "Son Güncelleyen" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__write_date +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__write_date +msgid "Last Updated on" +msgstr "Son Güncellenme" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__latitude +msgid "Latitude" +msgstr "Enlem" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__longitude +msgid "Longitude" +msgstr "Boylam" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_allow_time +msgid "Maximal Allowable Time (in hours)" +msgstr "İzin Verilen Maksimum Süre (saat olarak)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Maximum" +msgstr "Maksimum" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum Capacity" +msgstr "Maksimum Kapasite" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__max_order +msgid "Maximum Orders" +msgstr "Maksimum sipariş" + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route__max_order +msgid "Maximum number of orders per day route." +msgstr "Günlük rota başına maksimum sipariş sayısı." + +#. module: fieldservice_route +#: model:ir.model.fields,help:fieldservice_route.field_fsm_route_dayroute__max_order +msgid "Maximum numbers of orders that can be added to this day route." +msgstr "Bu günlük rotaya eklenebilecek maksimum sipariş sayısı." + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__monday +msgid "Monday" +msgstr "Pazartesi" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_day__name +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__name +msgid "Name" +msgstr "Adı" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +#: model:fsm.stage,name:fieldservice_route.fsm_stage_route_new +msgid "New" +msgstr "Yeni" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_count +msgid "Number of Orders" +msgstr "Sipariş Sayısı" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__order_ids +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Orders" +msgstr "Siparişler" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route__fsm_person_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__person_id +msgid "Person" +msgstr "Personel" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__date_start_planned +msgid "Planned Start Time" +msgstr "Planlanan Başlangıç Zamanı" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Planning" +msgstr "Planlama" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Positioning" +msgstr "Konumlandırma" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_location__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_order__fsm_route_id +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__route_id +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_stage__stage_type__route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "Route" +msgstr "Rota" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route_day +#: model:ir.model,name:fieldservice_route.model_fsm_route_day +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_day_form_view +msgid "Route Day" +msgstr "Rota Gün" + +#. module: fieldservice_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route_day +msgid "Route Days" +msgstr "Rota Günleri" + +#. module: fieldservice_route +#: model:ir.actions.act_window,name:fieldservice_route.action_fsm_route +#: model:ir.ui.menu,name:fieldservice_route.menu_fsm_route +msgid "Routes" +msgstr "Rotalar" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__saturday +msgid "Saturday" +msgstr "Cumartesi" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__stage_id +msgid "Stage" +msgstr "Aşama" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__start_location_id +msgid "Start Location" +msgstr "Başlama Konumu" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__sunday +msgid "Sunday" +msgstr "Pazar" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__team_id +msgid "Team" +msgstr "Ekip" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The day route is exceeding the maximum number of orders of the route." +msgstr "Günlük rota, rotanın maksimum sipariş sayısını aşıyor." + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "The route %(route_name)s does not run on %(name)s!" +msgstr "%(route_name)s isimli rota, %(name)s günleri çalışmıyor!" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__thursday +msgid "Thursday" +msgstr "Perşembe" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_route_dayroute__work_time +msgid "Time before overtime (in hours)" +msgstr "Fazla mesai öncesi süre (saat olarak)" + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_dayroute_form +msgid "Timing" +msgstr "Zamanlama" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__tuesday +msgid "Tuesday" +msgstr "Salı" + +#. module: fieldservice_route +#: model:ir.model.fields,field_description:fieldservice_route.field_fsm_stage__stage_type +msgid "Type" +msgstr "Tür" + +#. module: fieldservice_route +#: model:ir.model.fields.selection,name:fieldservice_route.selection__fsm_route_day__name__wednesday +msgid "Wednesday" +msgstr "Çarşamba" + +#. module: fieldservice_route +#. odoo-python +#: code:addons/fieldservice_route/models/fsm_route_dayroute.py:0 +msgid "You must create a FSM team first." +msgstr "Önce bir FSM ekibi oluşturmalısınız." + +#. module: fieldservice_route +#: model_terms:ir.ui.view,arch_db:fieldservice_route.fsm_route_form +msgid "orders per day route" +msgstr "günlük rota siparişleri" + +#~ msgid "Last Modified on" +#~ msgstr "Son Düzenlenme" + +#~ msgid "Order" +#~ msgstr "Sipariş" + +#, python-format +#~ msgid "The route %s does not run on %s!" +#~ msgstr "%s rotası %s üzerinde çalışmıyor!" diff --git a/fieldservice_route/models/__init__.py b/fieldservice_route/models/__init__.py new file mode 100644 index 0000000000..de911b39ca --- /dev/null +++ b/fieldservice_route/models/__init__.py @@ -0,0 +1,8 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import fsm_stage +from . import fsm_route_day +from . import fsm_route +from . import fsm_location +from . import fsm_route_dayroute +from . import fsm_order diff --git a/fieldservice_route/models/fsm_location.py b/fieldservice_route/models/fsm_location.py new file mode 100644 index 0000000000..717636ee9b --- /dev/null +++ b/fieldservice_route/models/fsm_location.py @@ -0,0 +1,24 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FSMLocation(models.Model): + _inherit = "fsm.location" + + fsm_route_id = fields.Many2one(comodel_name="fsm.route", string="Route") + + def write(self, vals): + res = super().write(vals) + if "fsm_route_id" in vals: + orders = self.env["fsm.order"].search( + [ + ("location_id", "in", self.ids), + ("scheduled_date_start", "!=", False), + ] + ) + for order in orders: + order._reassign_dayroute_for_person_timezone() + return res diff --git a/fieldservice_route/models/fsm_order.py b/fieldservice_route/models/fsm_order.py new file mode 100644 index 0000000000..b1393863c7 --- /dev/null +++ b/fieldservice_route/models/fsm_order.py @@ -0,0 +1,380 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import datetime, time, timedelta + +from pytz import timezone, utc + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class FSMOrder(models.Model): + _inherit = "fsm.order" + + _ROUTE_WRITE_FIELDS = frozenset( + { + "person_id", + "scheduled_date_start", + "scheduled_date_end", + "scheduled_duration", + "location_id", + "dayroute_id", + } + ) + + dayroute_id = fields.Many2one( + comodel_name="fsm.route.dayroute", string="Day Route", index=True + ) + fsm_route_id = fields.Many2one(related="location_id.fsm_route_id", string="Route") + + person_id = fields.Many2one( + comodel_name="fsm.person", + string="Assigned To", + index=True, + compute="_compute_person_id", + store=True, + readonly=False, + ) + + @api.depends("fsm_route_id", "fsm_route_id.fsm_person_id") + def _compute_person_id(self): + for item in self.filtered("fsm_route_id"): + item.person_id = item.fsm_route_id.fsm_person_id + + def prepare_dayroute_values(self, values): + return { + "person_id": values["person_id"], + "date": values["date"], + "route_id": values["route_id"], + } + + def _get_route_id_from_vals(self, vals): + if vals.get("fsm_route_id"): + return vals["fsm_route_id"] + location_id = vals.get("location_id") or self.location_id.id + if location_id: + return self.env["fsm.location"].browse(location_id).fsm_route_id.id + return self.fsm_route_id.id + + def _route_id_changing(self, vals, route_id): + """True when vals actually change the order's effective route.""" + if "location_id" not in vals and "fsm_route_id" not in vals: + return False + return (route_id or False) != (self.fsm_route_id.id or False) + + def _get_person_id_for_dayroute(self, vals, route_id): + if "person_id" in vals: + return vals.get("person_id") or False + # Location/route changes adopt the new route worker (or clear when + # unstaffed). Reschedules keep a manually assigned worker. + if self._route_id_changing(vals, route_id): + if route_id: + route = self.env["fsm.route"].browse(route_id) + return route.fsm_person_id.id if route.fsm_person_id else False + return self.person_id.id or False + if self.person_id: + return self.person_id.id + if route_id: + route = self.env["fsm.route"].browse(route_id) + if route.fsm_person_id: + return route.fsm_person_id.id + return self.fsm_route_id.fsm_person_id.id + + def _tz_name_for_route_day(self, person=None, route=None): + person = person or self.env["fsm.person"] + route = route or self.env["fsm.route"] + return ( + (person.partner_id.tz if person else False) + or (route.fsm_person_id.partner_id.tz if route.fsm_person_id else False) + or self.env.user.tz + or "UTC" + ) + + def _local_date_from_scheduled_start( + self, scheduled_start, person=None, route=None + ): + """Return the worker/route local calendar day for a UTC-naive datetime.""" + start = fields.Datetime.to_datetime(scheduled_start) + if not start: + return False + tzinfo = timezone(self._tz_name_for_route_day(person=person, route=route)) + return utc.localize(start).astimezone(tzinfo).date() + + def _scheduled_start_on_dayroute_date(self, current_start, dayroute, person=None): + """Move a UTC-naive start onto the dayroute date in the worker timezone.""" + person = person or dayroute.person_id + tzinfo = timezone( + self._tz_name_for_route_day(person=person, route=dayroute.route_id) + ) + if current_start: + local = utc.localize(fields.Datetime.to_datetime(current_start)).astimezone( + tzinfo + ) + # Re-localize on the target date so DST offset is recalculated. + naive_local = local.replace(tzinfo=None).replace( + year=dayroute.date.year, + month=dayroute.date.month, + day=dayroute.date.day, + ) + return tzinfo.localize(naive_local).astimezone(utc).replace(tzinfo=None) + if dayroute.date_start_planned: + return dayroute.date_start_planned + local = tzinfo.localize(datetime.combine(dayroute.date, time(8, 0))) + return local.astimezone(utc).replace(tzinfo=None) + + def _capture_duration_from_start_end(self, write_vals): + """Store duration from supplied start/end before the start date is moved.""" + if "scheduled_duration" in write_vals: + return write_vals + start = write_vals.get("scheduled_date_start") + end = write_vals.get("scheduled_date_end") + if not (start and end): + return write_vals + start_dt = fields.Datetime.to_datetime(start) + end_dt = fields.Datetime.to_datetime(end) + write_vals["scheduled_duration"] = (end_dt - start_dt).total_seconds() / 3600 + return write_vals + + def _duration_for_schedule_sync(self, write_vals): + if "scheduled_duration" in write_vals: + return write_vals.get("scheduled_duration") or 0.0 + start = write_vals.get("scheduled_date_start") + end = write_vals.get("scheduled_date_end") + if start and end: + start_dt = fields.Datetime.to_datetime(start) + end_dt = fields.Datetime.to_datetime(end) + return (end_dt - start_dt).total_seconds() / 3600 + return self.scheduled_duration or 0.0 + + def _sync_scheduled_end_from_start(self, write_vals): + """Keep end/duration aligned after scheduled_date_start is established.""" + start = write_vals.get("scheduled_date_start") + if not start: + return write_vals + start = fields.Datetime.to_datetime(start) + duration = self._duration_for_schedule_sync(write_vals) + write_vals["scheduled_duration"] = duration + write_vals["scheduled_date_end"] = start + timedelta(hours=duration) + return write_vals + + def _get_dayroute_values(self, vals): + route_id = self._get_route_id_from_vals(vals) + person_id = self._get_person_id_for_dayroute(vals, route_id) + person = ( + self.env["fsm.person"].browse(person_id) if person_id else self.person_id + ) + route = ( + self.env["fsm.route"].browse(route_id) if route_id else self.fsm_route_id + ) + scheduled_start = vals.get("scheduled_date_start") or self.scheduled_date_start + date = self._local_date_from_scheduled_start( + scheduled_start, person=person, route=route + ) + return { + "person_id": person_id, + "date": date, + "route_id": route_id, + } + + def _get_dayroute_domain(self, values): + return [ + ("person_id", "=", values["person_id"]), + ("date", "=", values["date"]), + ("route_id", "=", values.get("route_id") or False), + ("order_remaining", ">", 0), + ] + + def _dayroute_matches_values(self, dayroute, values): + return bool( + dayroute + and dayroute.person_id.id == values["person_id"] + and dayroute.date == values["date"] + and (dayroute.route_id.id or False) == (values.get("route_id") or False) + ) + + def _can_create_dayroute(self, values): + return values["person_id"] and values["date"] + + def _unlink_empty_dayroutes(self, dayroutes): + # Automatic cleanup must work for FSM users who can reschedule but + # do not have unlink rights on day routes. + empty = dayroutes.filtered(lambda dayroute: dayroute and not dayroute.order_ids) + empty.sudo().unlink() + + def _manage_fsm_route(self, vals): + dayroute_obj = self.env["fsm.route.dayroute"] + values = self._get_dayroute_values(vals) + # Keep the current day route when it still matches: the order already + # occupies a slot, so order_remaining may be 0 on a full route. + if self._dayroute_matches_values(self.dayroute_id, values): + vals.update({"dayroute_id": self.dayroute_id.id}) + return vals + domain = self._get_dayroute_domain(values) + dayroute = dayroute_obj.search(domain, limit=1) + if dayroute: + vals.update({"dayroute_id": dayroute.id}) + elif self._can_create_dayroute(values): + dayroute = dayroute_obj.create(self.prepare_dayroute_values(values)) + vals.update({"dayroute_id": dayroute.id}) + else: + # e.g. moved to an unstaffed route: drop the stale day route. + vals.update({"dayroute_id": False}) + if not values["person_id"] and self._route_id_changing( + vals, values.get("route_id") + ): + vals["person_id"] = False + return vals + + def _reassign_dayroute_for_person_timezone(self): + """Re-evaluate dayroute after the worker/timezone or route changes.""" + self.ensure_one() + if not self.scheduled_date_start: + return + if not self.person_id: + if self.dayroute_id: + self.write({"dayroute_id": False}) + return + old_dayroute = self.dayroute_id + managed = self._manage_fsm_route( + { + "person_id": self.person_id.id, + "scheduled_date_start": self.scheduled_date_start, + } + ) + new_dayroute_id = managed.get("dayroute_id") + if new_dayroute_id and new_dayroute_id != old_dayroute.id: + self.write({"dayroute_id": new_dayroute_id}) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get("fsm_route_id") and vals.get("location_id"): + location = self.env["fsm.location"].browse(vals["location_id"]) + vals.update({"fsm_route_id": location.fsm_route_id.id}) + + if vals.get("dayroute_id"): + order = self.new(vals) + dayroute = self.env["fsm.route.dayroute"].browse(vals["dayroute_id"]) + vals.update(order._prepare_vals_from_dayroute(dayroute, vals)) + elif vals.get("person_id") and vals.get("scheduled_date_start"): + vals = self._manage_fsm_route(vals) + return super().create(vals_list) + + def _prepare_route_write_vals(self, vals): + write_vals = dict(vals) + if { + "scheduled_date_end", + "scheduled_duration", + "scheduled_date_start", + } & write_vals.keys(): + self._calc_scheduled_dates(write_vals) + # Include start when only end was derived so super().write()'s + # second _calc_scheduled_dates takes the start+end branch. + # Otherwise duration 0 is falsy and the end-only branch recomputes + # start from the still-unwritten previous duration. + if ( + write_vals.get("scheduled_date_end") + and "scheduled_date_start" not in write_vals + and self.scheduled_date_start + ): + write_vals["scheduled_date_start"] = self.scheduled_date_start + return write_vals + + def _is_unscheduling(self, vals): + return "scheduled_date_start" in vals and not vals.get("scheduled_date_start") + + def _order_route_for_dayroute_write(self, write_vals): + location_id = write_vals.get("location_id") or self.location_id.id + if location_id: + return self.env["fsm.location"].browse(location_id).fsm_route_id + return self.fsm_route_id + + def _prepare_vals_from_dayroute(self, dayroute, write_vals): + """Make an explicit day route authoritative for person and schedule date.""" + write_vals = dict(write_vals) + if not dayroute: + return write_vals + order_route = self._order_route_for_dayroute_write(write_vals) + dayroute_route_id = dayroute.route_id.id if dayroute.route_id else False + order_route_id = order_route.id if order_route else False + if dayroute_route_id != order_route_id: + raise ValidationError( + self.env._( + "The day route %(dayroute)s belongs to route %(route)s, " + "which does not match the order route %(order_route)s.", + dayroute=dayroute.display_name, + route=dayroute.route_id.display_name + if dayroute.route_id + else self.env._("no route"), + order_route=order_route.display_name + if order_route + else self.env._("no route"), + ) + ) + if dayroute.person_id: + write_vals["person_id"] = dayroute.person_id.id + if dayroute.date: + write_vals = self._capture_duration_from_start_end(write_vals) + current_start = ( + write_vals.get("scheduled_date_start") or self.scheduled_date_start + ) + write_vals["scheduled_date_start"] = self._scheduled_start_on_dayroute_date( + current_start, + dayroute, + person=dayroute.person_id, + ) + write_vals = self._sync_scheduled_end_from_start(write_vals) + return write_vals + + def _write_with_route_management(self, vals): + self.ensure_one() + write_vals = self._prepare_route_write_vals(vals) + dayroutes_to_check = self.env["fsm.route.dayroute"] + + if self._is_unscheduling(write_vals): + dayroutes_to_check |= self.dayroute_id + write_vals["dayroute_id"] = False + res = super().write(write_vals) + self._unlink_empty_dayroutes(dayroutes_to_check) + return res + + if "person_id" in write_vals and not write_vals.get("person_id"): + dayroutes_to_check |= self.dayroute_id + write_vals["dayroute_id"] = False + res = super().write(write_vals) + self._unlink_empty_dayroutes(dayroutes_to_check) + return res + + if "dayroute_id" in write_vals: + dayroutes_to_check |= self.dayroute_id + new_dayroute = self.env["fsm.route.dayroute"].browse( + write_vals.get("dayroute_id") or [] + ) + write_vals = self._prepare_vals_from_dayroute(new_dayroute, write_vals) + res = super().write(write_vals) + self._unlink_empty_dayroutes(dayroutes_to_check) + return res + + if (write_vals.get("person_id") or self.person_id) and ( + write_vals.get("scheduled_date_start") or self.scheduled_date_start + ): + dayroutes_to_check |= self.dayroute_id + write_vals = self._manage_fsm_route(write_vals) + + res = super().write(write_vals) + self._unlink_empty_dayroutes(dayroutes_to_check) + return res + + def write(self, vals): + route_trigger_fields = self._ROUTE_WRITE_FIELDS | { + "scheduled_date_end", + "scheduled_duration", + } + if not route_trigger_fields.intersection(vals): + return super().write(vals) + if len(self) == 1: + return self._write_with_route_management(vals) + for order in self: + order._write_with_route_management(vals) + return True diff --git a/fieldservice_route/models/fsm_route.py b/fieldservice_route/models/fsm_route.py new file mode 100644 index 0000000000..f3967a6234 --- /dev/null +++ b/fieldservice_route/models/fsm_route.py @@ -0,0 +1,41 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class FSMRoute(models.Model): + _name = "fsm.route" + _description = "Field Service Route" + + name = fields.Char(required=True) + fsm_person_id = fields.Many2one(comodel_name="fsm.person", string="Person") + day_ids = fields.Many2many(comodel_name="fsm.route.day", string="Days") + max_order = fields.Integer( + string="Maximum Orders", + default=0, + help="Maximum number of orders per day route.", + ) + + def run_on(self, date): + """ + :param date: date + :return: True if the route runs on the date, False otherwise. + """ + if date: + day_index = date.weekday() + day = self.env.ref("fieldservice_route.fsm_route_day_" + str(day_index)) + return day in self.day_ids + + def write(self, vals): + res = super().write(vals) + if "fsm_person_id" in vals: + orders = self.env["fsm.order"].search( + [ + ("fsm_route_id", "in", self.ids), + ("scheduled_date_start", "!=", False), + ] + ) + for order in orders: + order._reassign_dayroute_for_person_timezone() + return res diff --git a/fieldservice_route/models/fsm_route_day.py b/fieldservice_route/models/fsm_route_day.py new file mode 100644 index 0000000000..c469c46495 --- /dev/null +++ b/fieldservice_route/models/fsm_route_day.py @@ -0,0 +1,22 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FSMRouteDay(models.Model): + _name = "fsm.route.day" + _description = "Route Day" + + name = fields.Selection( + selection=[ + ("Monday", "Monday"), + ("Tuesday", "Tuesday"), + ("Wednesday", "Wednesday"), + ("Thursday", "Thursday"), + ("Friday", "Friday"), + ("Saturday", "Saturday"), + ("Sunday", "Sunday"), + ], + ) diff --git a/fieldservice_route/models/fsm_route_dayroute.py b/fieldservice_route/models/fsm_route_dayroute.py new file mode 100644 index 0000000000..a8e6cc19eb --- /dev/null +++ b/fieldservice_route/models/fsm_route_dayroute.py @@ -0,0 +1,236 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import datetime, time, timedelta + +from pytz import timezone, utc + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +_CALENDAR_DEPENDS = ( + "tz", + "two_weeks_calendar", + "attendance_ids.hour_from", + "attendance_ids.hour_to", + "attendance_ids.dayofweek", + "attendance_ids.week_type", + "attendance_ids.day_period", + "attendance_ids.display_type", +) + + +def _calendar_depends(prefix): + return [f"{prefix}.{field_name}" for field_name in _CALENDAR_DEPENDS] + + +class FSMRouteDayRoute(models.Model): + _name = "fsm.route.dayroute" + _description = "Field Service Route Dayroute" + + name = fields.Char( + required=True, copy=False, default=lambda self: self.env._("New") + ) + person_id = fields.Many2one( + comodel_name="fsm.person", + string="Person", + compute="_compute_person_id", + store=True, + readonly=False, + ) + route_id = fields.Many2one(comodel_name="fsm.route", string="Route") + date = fields.Date(required=True) + team_id = fields.Many2one( + comodel_name="fsm.team", + string="Team", + default=lambda self: self._default_team_id(), + ) + stage_id = fields.Many2one( + comodel_name="fsm.stage", + string="Stage", + domain="[('stage_type', '=', 'route')]", + index=True, + copy=False, + default=lambda self: self._default_stage_id(), + ) + longitude = fields.Float() + latitude = fields.Float() + last_location_id = fields.Many2one( + comodel_name="fsm.location", string="Last Location" + ) + date_start_planned = fields.Datetime( + string="Planned Start Time", + compute="_compute_date_start_planned", + store=True, + readonly=False, + ) + start_location_id = fields.Many2one( + comodel_name="fsm.location", string="Start Location" + ) + end_location_id = fields.Many2one( + comodel_name="fsm.location", string="End Location" + ) + work_time = fields.Float(string="Time before overtime (in hours)", default=8.0) + max_allow_time = fields.Float( + string="Maximal Allowable Time (in hours)", default=10.0 + ) + order_ids = fields.One2many( + comodel_name="fsm.order", inverse_name="dayroute_id", string="Orders" + ) + order_count = fields.Integer( + compute="_compute_order_count", string="Number of Orders", store=True + ) + order_remaining = fields.Integer( + compute="_compute_order_count", string="Available Capacity", store=True + ) + max_order = fields.Integer( + related="route_id.max_order", + string="Maximum Capacity", + store=True, + help="Maximum numbers of orders that can be added to this day route.", + ) + + def _default_team_id(self): + teams = self.env["fsm.team"].search( + [("company_id", "in", (self.env.company.id, False))], + order="sequence asc", + limit=1, + ) + if teams: + return teams + raise ValidationError(self.env._("You must create a FSM team first.")) + + def _default_stage_id(self): + return self.env["fsm.stage"].search( + [("stage_type", "=", "route"), ("is_default", "=", True)], limit=1 + ) + + @api.model + def _planned_start_from_date(self, route_date, person=None, route=None, team=None): + """Return planned start as UTC naive datetime for the given route date.""" + route_date = fields.Date.to_date(route_date) + if route and not person: + person = route.fsm_person_id + company = team.company_id if team else self.env.company + calendar = (person and person.calendar_id) or company.resource_calendar_id + tz_name = ( + (person and person.partner_id.tz) + or (calendar and calendar.tz) + or self.env.user.tz + or "UTC" + ) + tzinfo = timezone(tz_name) + day_start = tzinfo.localize(datetime.combine(route_date, time.min)) + # Localize next midnight so DST transitions get the correct offset. + day_end = tzinfo.localize( + datetime.combine(route_date + timedelta(days=1), time.min) + ) + if calendar: + work_time = calendar._get_closest_work_time( + day_start, + match_end=False, + search_range=(day_start, day_end), + compute_leaves=False, + ) + if work_time: + return work_time.astimezone(utc).replace(tzinfo=None) + fallback = tzinfo.localize(datetime.combine(route_date, time(8, 0))) + return fallback.astimezone(utc).replace(tzinfo=None) + + @api.depends("route_id", "route_id.max_order", "max_order", "order_ids") + def _compute_order_count(self): + for rec in self: + rec.order_count = len(rec.order_ids) + rec.order_remaining = rec.max_order - rec.order_count + + @api.depends("route_id", "route_id.fsm_person_id") + def _compute_person_id(self): + for rec in self: + if not rec.route_id.fsm_person_id: + rec.person_id = None + continue + + rec.person_id = rec.route_id.fsm_person_id + + @api.depends( + "date", + "person_id", + "person_id.partner_id.tz", + *(_calendar_depends("person_id.calendar_id")), + "route_id", + "route_id.fsm_person_id", + "route_id.fsm_person_id.partner_id.tz", + *(_calendar_depends("route_id.fsm_person_id.calendar_id")), + "team_id", + "team_id.company_id", + *(_calendar_depends("team_id.company_id.resource_calendar_id")), + ) + def _compute_date_start_planned(self): + for rec in self: + if not rec.date: + rec.date_start_planned = False + continue + rec.date_start_planned = rec._planned_start_from_date( + rec.date, + person=rec.person_id or rec.route_id.fsm_person_id, + route=rec.route_id, + team=rec.team_id, + ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name", self.env._("New")) == self.env._("New"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "fsm.route.dayroute" + ) or self.env._("New") + if not vals.get("date_start_planned") and vals.get("date"): + route = ( + self.env["fsm.route"].browse(vals["route_id"]) + if vals.get("route_id") + else self.env["fsm.route"] + ) + person = ( + self.env["fsm.person"].browse(vals["person_id"]) + if vals.get("person_id") + else self.env["fsm.person"] + ) + if vals.get("team_id"): + team = self.env["fsm.team"].browse(vals["team_id"]) + else: + team = self._default_team_id() + vals["team_id"] = team.id + vals["date_start_planned"] = self._planned_start_from_date( + vals["date"], + person=person, + route=route, + team=team, + ) + return super().create(vals_list) + + @api.constrains("date", "route_id") + def check_day(self): + for rec in self: + if rec.date and rec.route_id: + # Get the day of the week: Monday -> 0, Sunday -> 6 + day_index = rec.date.weekday() + day = self.env.ref("fieldservice_route.fsm_route_day_" + str(day_index)) + if day.id not in rec.route_id.day_ids.ids: + raise ValidationError( + self.env._( + "The route %(route_name)s does not run on %(name)s!", + route_name=rec.route_id.name, + name=day.name, + ) + ) + + @api.constrains("route_id", "max_order", "order_count") + def check_capacity(self): + for rec in self: + if rec.route_id and rec.order_count > rec.max_order: + raise ValidationError( + self.env._( + "The day route is exceeding the maximum number of " + "orders of the route." + ) + ) diff --git a/fieldservice_route/models/fsm_stage.py b/fieldservice_route/models/fsm_stage.py new file mode 100644 index 0000000000..6307a8bb68 --- /dev/null +++ b/fieldservice_route/models/fsm_stage.py @@ -0,0 +1,13 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FSMStage(models.Model): + _inherit = "fsm.stage" + + stage_type = fields.Selection( + selection_add=[("route", "Route")], ondelete={"route": "cascade"} + ) diff --git a/fieldservice_route/pyproject.toml b/fieldservice_route/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fieldservice_route/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fieldservice_route/readme/CONFIGURE.md b/fieldservice_route/readme/CONFIGURE.md new file mode 100644 index 0000000000..8cdd4c2cf1 --- /dev/null +++ b/fieldservice_route/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +To use this module, you need to: + +- Go to Field Service > Configuration > Stages +- Define the workflow of your routes by creating new stages whose type + is "Route" +- Go to Field Service > Master Data > Routes +- Create your routes by setting their name, worker, days, and capacity diff --git a/fieldservice_route/readme/CONTRIBUTORS.md b/fieldservice_route/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..4d6fbf32ae --- /dev/null +++ b/fieldservice_route/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- [Gray Matter Logic](https://www.graymatterlogic.com): + - Maxime Chambreuil \<\> +- Nikul Chaudhary \<\> +- Freni Patel \<\> +- [Tecnativa](https://www.tecnativa.com): + - Víctor Martínez diff --git a/fieldservice_route/readme/CREDITS.md b/fieldservice_route/readme/CREDITS.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fieldservice_route/readme/DESCRIPTION.md b/fieldservice_route/readme/DESCRIPTION.md new file mode 100644 index 0000000000..df38adf933 --- /dev/null +++ b/fieldservice_route/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +A Field Service route is the sorted list of orders that one +worker/vehicle will perform during a day. + +This module allows you to manage your Field Service routes: when +assigning an order to a worker and scheduling it, a route is +automatically created for that worker and day. diff --git a/fieldservice_route/readme/USAGE.md b/fieldservice_route/readme/USAGE.md new file mode 100644 index 0000000000..d5a6beaa33 --- /dev/null +++ b/fieldservice_route/readme/USAGE.md @@ -0,0 +1,7 @@ +To use this module, you need to: + +- Go to Field Service +- Create or select an order +- Assign it to a worker and schedule it +- Go to Field Service \> Dashboard \> Day Routes. A new record has been + created. diff --git a/fieldservice_route/security/ir.model.access.csv b/fieldservice_route/security/ir.model.access.csv new file mode 100644 index 0000000000..7e5364a684 --- /dev/null +++ b/fieldservice_route/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_route_user,fsm.route.user,model_fsm_route,fieldservice.group_fsm_user,1,0,0,0 +access_fsm_route_dispatcher,fsm.route.dispatcher,model_fsm_route,fieldservice.group_fsm_dispatcher,1,1,1,1 +access_fsm_route_day_user,fsm.route.day.user,model_fsm_route_day,fieldservice.group_fsm_user,1,0,0,0 +access_fsm_route_day_dispatcher,fsm.route.day.dispatcher,model_fsm_route_day,fieldservice.group_fsm_manager,1,1,0,0 +access_fsm_route_dayroute_user,fsm.route.dayroute.user,model_fsm_route_dayroute,fieldservice.group_fsm_user,1,1,1,0 +access_fsm_route_dayroute_dispatcher,fsm.route.dayroute.dispatcher,model_fsm_route_dayroute,fieldservice.group_fsm_dispatcher,1,1,1,1 +access_fsm_route_dayroute_portal,access.fsm.route.dayroute.portal,model_fsm_route_dayroute,base.group_portal,1,0,0,0 diff --git a/fieldservice_route/security/ir_rule.xml b/fieldservice_route/security/ir_rule.xml new file mode 100644 index 0000000000..256a3692ed --- /dev/null +++ b/fieldservice_route/security/ir_rule.xml @@ -0,0 +1,11 @@ + + + + FSM Route DayRoutes Entry (portal own) + + [('person_id.partner_id', '=', user.partner_id.id)] + + + diff --git a/fieldservice_route/static/description/icon.png b/fieldservice_route/static/description/icon.png new file mode 100644 index 0000000000..e3757caa15 Binary files /dev/null and b/fieldservice_route/static/description/icon.png differ diff --git a/fieldservice_route/static/description/icon.svg b/fieldservice_route/static/description/icon.svg new file mode 100644 index 0000000000..c27c090fda --- /dev/null +++ b/fieldservice_route/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fieldservice_route/static/description/index.html b/fieldservice_route/static/description/index.html new file mode 100644 index 0000000000..1a597e80f1 --- /dev/null +++ b/fieldservice_route/static/description/index.html @@ -0,0 +1,472 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Field Service Route

+ +

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

+

A Field Service route is the sorted list of orders that one +worker/vehicle will perform during a day.

+

This module allows you to manage your Field Service routes: when +assigning an order to a worker and scheduling it, a route is +automatically created for that worker and day.

+

Table of contents

+ +
+

Configuration

+

To use this module, you need to:

+
    +
  • Go to Field Service > Configuration > Stages
  • +
  • Define the workflow of your routes by creating new stages whose type +is “Route”
  • +
  • Go to Field Service > Master Data > Routes
  • +
  • Create your routes by setting their name, worker, days, and capacity
  • +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  • Go to Field Service
  • +
  • Create or select an order
  • +
  • Assign it to a worker and schedule it
  • +
  • Go to Field Service > Dashboard > Day Routes. A new record has been +created.
  • +
+
+
+

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

+
    +
  • Gray Matter Logic
  • +
+
+
+

Contributors

+ +
+ +
+

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.

+

Current maintainer:

+

max3903

+

This module is part of the OCA/field-service project on GitHub.

+

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

+
+
+
+
+ + diff --git a/fieldservice_route/tests/__init__.py b/fieldservice_route/tests/__init__.py new file mode 100644 index 0000000000..af4f716821 --- /dev/null +++ b/fieldservice_route/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_fsm_location, test_fsm_order, test_fsm_route, test_fsm_route_dayroute diff --git a/fieldservice_route/tests/test_fsm_location.py b/fieldservice_route/tests/test_fsm_location.py new file mode 100644 index 0000000000..e1a7627c4a --- /dev/null +++ b/fieldservice_route/tests/test_fsm_location.py @@ -0,0 +1,36 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.fieldservice.tests.test_fsm_common import FSMCommon + + +class TestFSMLocationRoute(FSMCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.route = cls.env["fsm.route"].create( + { + "name": "Location Route", + "max_order": 3, + "fsm_person_id": cls.test_person.id, + "day_ids": [ + (6, 0, [cls.env.ref("fieldservice_route.fsm_route_day_0").id]) + ], + } + ) + + def test_location_route_field(self): + self.test_location.fsm_route_id = self.route + self.assertEqual(self.test_location.fsm_route_id, self.route) + + def test_stage_route_type(self): + stage = self.env["fsm.stage"].create( + { + "name": "Route Stage", + "stage_type": "route", + "sequence": 99, + } + ) + self.assertEqual(stage.stage_type, "route") diff --git a/fieldservice_route/tests/test_fsm_order.py b/fieldservice_route/tests/test_fsm_order.py new file mode 100644 index 0000000000..58e708f648 --- /dev/null +++ b/fieldservice_route/tests/test_fsm_order.py @@ -0,0 +1,1521 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# Copyright 2022 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from pytz import timezone, utc + +from odoo.exceptions import ValidationError +from odoo.tests import Form +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +from odoo.addons.fieldservice.tests.test_fsm_common import FSMCommon + + +class TestFSMOrderRoute(FSMCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.fsm_route_obj = cls.env["fsm.route"] + date = datetime.now() + cls.date = date.replace(microsecond=0) + cls.days = [ + cls.env.ref("fieldservice_route.fsm_route_day_0").id, + cls.env.ref("fieldservice_route.fsm_route_day_1").id, + cls.env.ref("fieldservice_route.fsm_route_day_2").id, + cls.env.ref("fieldservice_route.fsm_route_day_3").id, + cls.env.ref("fieldservice_route.fsm_route_day_4").id, + cls.env.ref("fieldservice_route.fsm_route_day_5").id, + cls.env.ref("fieldservice_route.fsm_route_day_6").id, + ] + cls.fsm_route_id = cls.fsm_route_obj.create( + { + "name": "Demo Route", + "max_order": 10, + "fsm_person_id": cls.test_person.id, + "day_ids": [(6, 0, cls.days)], + } + ) + cls.test_location.fsm_route_id = cls.fsm_route_id.id + + def test_create_day_route(self): + order_form = Form(self.env["fsm.order"]) + order_form.location_id = self.test_location + order_form.scheduled_date_start = self.date + order = order_form.save() + self.assertEqual(order.person_id, self.test_person) + self.assertEqual(order.fsm_route_id, self.test_location.fsm_route_id) + self.assertEqual(order.dayroute_id.person_id, order.person_id) + self.assertEqual(order.dayroute_id.date, order.scheduled_date_start.date()) + self.assertEqual(order.dayroute_id.route_id, order.fsm_route_id) + + def test_date_start_planned_uses_worker_schedule(self): + route_date = self.date.date() + calendar = self.env["resource.calendar"].create( + { + "name": "Early Shift", + "tz": "US/Eastern", + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Shift", + "dayofweek": str(route_date.weekday()), + "hour_from": 6.0, + "hour_to": 14.0, + "day_period": "morning", + }, + ) + ], + } + ) + self.test_person.partner_id.tz = "US/Eastern" + self.test_person.calendar_id = calendar + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": route_date, + } + ) + actual_local = utc.localize(dayroute.date_start_planned).astimezone( + timezone("US/Eastern") + ) + self.assertEqual(actual_local.hour, 6) + self.assertEqual(actual_local.minute, 0) + + def test_date_start_planned_recomputes_on_calendar_change(self): + route_date = self.date.date() + calendar = self.env["resource.calendar"].create( + { + "name": "Morning Shift", + "tz": "US/Eastern", + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Shift", + "dayofweek": str(route_date.weekday()), + "hour_from": 7.0, + "hour_to": 15.0, + "day_period": "morning", + }, + ) + ], + } + ) + self.test_person.partner_id.tz = "US/Eastern" + self.test_person.calendar_id = calendar + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": route_date, + } + ) + before = utc.localize(dayroute.date_start_planned).astimezone( + timezone("US/Eastern") + ) + self.assertEqual(before.hour, 7) + calendar.attendance_ids.write({"hour_from": 9.0}) + after = utc.localize(dayroute.date_start_planned).astimezone( + timezone("US/Eastern") + ) + self.assertEqual(after.hour, 9) + + def test_date_start_planned_fallback_without_calendar(self): + route_date = self.date.date() + empty_calendar = self.env["resource.calendar"].create( + { + "name": "Empty Schedule", + "tz": "US/Eastern", + "attendance_ids": [], + } + ) + self.test_person.calendar_id = empty_calendar + self.test_person.partner_id.tz = "US/Eastern" + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": route_date, + } + ) + actual_local = utc.localize(dayroute.date_start_planned).astimezone( + timezone("US/Eastern") + ) + self.assertEqual(actual_local.hour, 8) + self.assertEqual(actual_local.minute, 0) + + def test_reuse_existing_dayroute(self): + order1 = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order2 = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order1.dayroute_id, order2.dayroute_id) + self.assertEqual(order1.dayroute_id.order_count, 2) + + def test_order_person_from_route(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + } + ) + self.assertEqual(order.person_id, self.fsm_route_id.fsm_person_id) + + def test_order_sets_route_from_location(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + } + ) + self.assertEqual(order.fsm_route_id, self.test_location.fsm_route_id) + + def test_order_write_scheduled_date_start(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + dayroute = order.dayroute_id + new_date = self.date + timedelta(days=1) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + order.write({"scheduled_date_start": new_date}) + self.assertNotEqual(order.dayroute_id, dayroute) + self.assertEqual(order.dayroute_id.date, new_date.date()) + + def test_get_dayroute_values_string_datetime(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + values = order._get_dayroute_values( + { + "scheduled_date_start": self.date.strftime( + DEFAULT_SERVER_DATETIME_FORMAT + ), + "person_id": self.test_person.id, + } + ) + self.assertEqual(values["date"], self.date.date()) + self.assertEqual(values["person_id"], self.test_person.id) + + def test_prepare_dayroute_values_and_domain(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + values = { + "person_id": self.test_person.id, + "date": self.date.date(), + "route_id": self.fsm_route_id.id, + } + prepared = order.prepare_dayroute_values(values) + self.assertEqual(prepared, values) + domain = order._get_dayroute_domain(values) + self.assertEqual( + domain, + [ + ("person_id", "=", self.test_person.id), + ("date", "=", self.date.date()), + ("route_id", "=", self.fsm_route_id.id), + ("order_remaining", ">", 0), + ], + ) + routeless_domain = order._get_dayroute_domain( + { + "person_id": self.test_person.id, + "date": self.date.date(), + "route_id": False, + } + ) + self.assertEqual( + routeless_domain, + [ + ("person_id", "=", self.test_person.id), + ("date", "=", self.date.date()), + ("route_id", "=", False), + ("order_remaining", ">", 0), + ], + ) + self.assertTrue(order._can_create_dayroute(values)) + self.assertFalse( + order._can_create_dayroute({"person_id": False, "date": False}) + ) + + def test_manage_fsm_route_skips_create_without_worker(self): + route = self.fsm_route_obj.create( + { + "name": "Route Without Worker", + "max_order": 5, + "day_ids": [(6, 0, self.days)], + } + ) + location = self.env["fsm.location"].create( + { + "name": "Route Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + order = self.env["fsm.order"].new({"location_id": location.id}) + vals = order._manage_fsm_route({"scheduled_date_start": self.date}) + self.assertFalse(vals.get("dayroute_id")) + + def test_manage_fsm_route_unlinks_empty_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + new_date = self.date + timedelta(days=7) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + order.write({"scheduled_date_start": new_date}) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_two_routes_same_worker_same_date(self): + other_route = self.fsm_route_obj.create( + { + "name": "Second Demo Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + other_location = self.env["fsm.location"].create( + { + "name": "Second Route Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": other_route.id, + } + ) + order1 = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order2 = self.env["fsm.order"].create( + { + "location_id": other_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertNotEqual(order1.dayroute_id, order2.dayroute_id) + self.assertEqual(order1.dayroute_id.route_id, self.fsm_route_id) + self.assertEqual(order2.dayroute_id.route_id, other_route) + + def test_get_dayroute_values_from_record_scheduled_date(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + } + ) + values = order._get_dayroute_values({}) + self.assertEqual(values["date"], self.date.date()) + self.assertEqual(values["route_id"], self.fsm_route_id.id) + + def test_get_dayroute_values_datetime_object(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + values = order._get_dayroute_values( + { + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + "fsm_route_id": self.fsm_route_id.id, + } + ) + self.assertEqual(values["date"], self.date.date()) + + def test_create_without_dayroute_assignment(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + } + ) + self.assertFalse(order.dayroute_id) + + def test_create_only_scheduled_without_person(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + } + ) + self.assertFalse(order.dayroute_id) + + def test_write_skips_route_management(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + dayroute = order.dayroute_id + order.write({"description": "No route update"}) + self.assertEqual(order.dayroute_id, dayroute) + + def test_bulk_write_description_keeps_dayroutes(self): + other_route = self.fsm_route_obj.create( + { + "name": "Bulk Write Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + other_location = self.env["fsm.location"].create( + { + "name": "Bulk Write Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": other_route.id, + } + ) + order1 = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order2 = self.env["fsm.order"].create( + { + "location_id": other_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + dayroute1 = order1.dayroute_id + dayroute2 = order2.dayroute_id + (order1 + order2).write({"description": "Bulk update"}) + self.assertEqual(order1.dayroute_id, dayroute1) + self.assertEqual(order2.dayroute_id, dayroute2) + self.assertNotEqual(dayroute1, dayroute2) + + def test_write_clear_scheduled_date_clears_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + order.write({"scheduled_date_start": False}) + self.assertFalse(order.scheduled_date_start) + self.assertFalse(order.dayroute_id) + self.assertFalse(old_dayroute.exists()) + + def test_write_location_change_updates_dayroute(self): + other_route = self.fsm_route_obj.create( + { + "name": "Location Change Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + other_location = self.env["fsm.location"].create( + { + "name": "Location Change Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": other_route.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + order.write({"location_id": other_location.id}) + self.assertEqual(order.fsm_route_id, other_route) + self.assertEqual(order.dayroute_id.route_id, other_route) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_routeless_order_does_not_reuse_routed_dayroute(self): + routeless_location = self.env["fsm.location"].create( + { + "name": "Routeless Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + } + ) + routed_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + order = self.env["fsm.order"].create( + { + "location_id": routeless_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertNotEqual(order.dayroute_id, routed_dayroute) + self.assertFalse(order.dayroute_id.route_id) + + def test_write_scheduled_date_end_updates_dayroute(self): + monday = self.date + while monday.weekday() != 0: + monday -= timedelta(days=1) + tuesday = monday + timedelta(days=1) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": monday.replace(hour=22, minute=0), + "scheduled_duration": 4.0, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order.dayroute_id.date, monday.date()) + order.write( + { + "scheduled_date_end": tuesday.replace(hour=18, minute=0), + } + ) + self.assertEqual(order.scheduled_date_start.date(), tuesday.date()) + self.assertEqual(order.dayroute_id.date, tuesday.date()) + + def test_write_direct_dayroute_id_clears_old_dayroute(self): + self.test_person.partner_id.tz = "UTC" + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + original_local = utc.localize(order.scheduled_date_start).astimezone( + timezone("UTC") + ) + new_date = self.date + timedelta(days=7) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + new_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": new_date.date(), + } + ) + order.write({"dayroute_id": new_dayroute.id}) + self.assertEqual(order.dayroute_id, new_dayroute) + local_after = utc.localize(order.scheduled_date_start).astimezone( + timezone("UTC") + ) + self.assertEqual(local_after.date(), new_dayroute.date) + self.assertEqual(local_after.time(), original_local.time()) + self.assertEqual(order.person_id, new_dayroute.person_id) + self.assertFalse(old_dayroute.exists()) + + def test_write_direct_dayroute_rejects_route_mismatch(self): + other_route = self.fsm_route_obj.create( + { + "name": "Mismatch Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + other_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": other_route.id, + "date": self.date.date(), + } + ) + with self.assertRaises(ValidationError): + order.write({"dayroute_id": other_dayroute.id}) + + def test_write_zero_duration_keeps_dayroute_date(self): + tuesday = self.date + while tuesday.weekday() != 1: + tuesday += timedelta(days=1) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": tuesday.replace(hour=0, minute=30), + "scheduled_duration": 2.0, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order.dayroute_id.date, tuesday.date()) + order.write({"scheduled_duration": 0.0}) + self.assertEqual(order.scheduled_date_start.date(), tuesday.date()) + self.assertEqual(order.dayroute_id.date, tuesday.date()) + self.assertEqual(order.scheduled_duration, 0.0) + + def test_write_direct_dayroute_removal_clears_old_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + order.write({"dayroute_id": False}) + self.assertFalse(order.dayroute_id) + self.assertFalse(old_dayroute.exists()) + + def test_order_create_multi(self): + # Use midday so +1h cannot cross a local/UTC midnight day boundary. + self.test_person.partner_id.tz = "UTC" + start = self.date.replace(hour=12, minute=0, second=0, microsecond=0) + orders = self.env["fsm.order"].create( + [ + { + "location_id": self.test_location.id, + "scheduled_date_start": start, + "person_id": self.test_person.id, + }, + { + "location_id": self.test_location.id, + "scheduled_date_start": start + timedelta(hours=1), + "person_id": self.test_person.id, + }, + ] + ) + self.assertEqual(len(orders), 2) + self.assertEqual(orders[0].dayroute_id, orders[1].dayroute_id) + self.assertEqual(orders[0].dayroute_id.date, start.date()) + + def test_get_route_id_from_vals_prefers_fsm_route_id(self): + other_route = self.fsm_route_obj.create( + { + "name": "Explicit Route", + "max_order": 5, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + self.assertEqual( + order._get_route_id_from_vals({"fsm_route_id": other_route.id}), + other_route.id, + ) + + def test_get_route_id_from_vals_without_location(self): + order = self.env["fsm.order"].new({}) + self.assertFalse(order._get_route_id_from_vals({})) + + def test_get_person_id_for_dayroute_from_route(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + person_id = order._get_person_id_for_dayroute({}, self.fsm_route_id.id) + self.assertEqual(person_id, self.test_person.id) + + def test_get_person_id_for_dayroute_from_order_person(self): + other_person = self.env["fsm.person"].create({"name": "Fallback Worker"}) + order = self.env["fsm.order"].new( + { + "location_id": self.test_location.id, + "person_id": other_person.id, + } + ) + person_id = order._get_person_id_for_dayroute({}, False) + self.assertEqual(person_id, other_person.id) + + def test_write_person_id_reassigns_dayroute(self): + other_person = self.env["fsm.person"].create({"name": "Other Assignee"}) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + order.write({"person_id": other_person.id}) + self.assertEqual(order.dayroute_id.person_id, other_person) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_bulk_write_scheduled_date_keeps_separate_dayroutes(self): + other_route = self.fsm_route_obj.create( + { + "name": "Bulk Schedule Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + other_location = self.env["fsm.location"].create( + { + "name": "Bulk Schedule Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": other_route.id, + } + ) + order1 = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order2 = self.env["fsm.order"].create( + { + "location_id": other_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + new_date = self.date + timedelta(days=1) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + (order1 + order2).write({"scheduled_date_start": new_date}) + self.assertEqual(order1.dayroute_id.date, new_date.date()) + self.assertEqual(order2.dayroute_id.date, new_date.date()) + self.assertNotEqual(order1.dayroute_id, order2.dayroute_id) + self.assertEqual(order1.dayroute_id.route_id, self.fsm_route_id) + self.assertEqual(order2.dayroute_id.route_id, other_route) + + def test_write_direct_dayroute_uses_planned_start_when_unscheduled(self): + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + } + ) + self.assertFalse(order.scheduled_date_start) + order.write({"dayroute_id": dayroute.id}) + self.assertEqual(order.dayroute_id, dayroute) + self.assertEqual(order.scheduled_date_start, dayroute.date_start_planned) + self.assertEqual(order.person_id, dayroute.person_id) + + def test_write_direct_dayroute_uses_date_when_no_planned_start(self): + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + dayroute.write({"date_start_planned": False}) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + } + ) + order.write({"dayroute_id": dayroute.id}) + self.assertEqual(order.scheduled_date_start.date(), dayroute.date) + + def test_order_route_for_dayroute_write_without_location(self): + order = self.env["fsm.order"].new({}) + self.assertFalse(order._order_route_for_dayroute_write({})) + + def test_prepare_vals_from_dayroute_empty(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + vals = order._prepare_vals_from_dayroute( + self.env["fsm.route.dayroute"], {"dayroute_id": False} + ) + self.assertEqual(vals, {"dayroute_id": False}) + + def test_create_with_full_dayroute_creates_another(self): + route = self.fsm_route_obj.create( + { + "name": "Capacity One", + "max_order": 1, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + location = self.env["fsm.location"].create( + { + "name": "Capacity Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + order1 = self.env["fsm.order"].create( + { + "location_id": location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order2 = self.env["fsm.order"].create( + { + "location_id": location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertNotEqual(order1.dayroute_id, order2.dayroute_id) + self.assertEqual(order1.dayroute_id.order_count, 1) + self.assertEqual(order2.dayroute_id.order_count, 1) + + def test_write_location_and_dayroute_together(self): + other_route = self.fsm_route_obj.create( + { + "name": "Combined Write Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + other_location = self.env["fsm.location"].create( + { + "name": "Combined Write Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": other_route.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + new_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": other_route.id, + "date": self.date.date(), + } + ) + order.write( + { + "location_id": other_location.id, + "dayroute_id": new_dayroute.id, + } + ) + self.assertEqual(order.location_id, other_location) + self.assertEqual(order.dayroute_id, new_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_unlink_empty_dayroutes_ignores_empty_recordset(self): + order = self.env["fsm.order"].new({}) + order._unlink_empty_dayroutes(self.env["fsm.route.dayroute"]) + + def test_route_worker_change_updates_order_person(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + } + ) + self.assertEqual(order.person_id, self.test_person) + other_person = self.env["fsm.person"].create({"name": "New Route Worker"}) + self.fsm_route_id.fsm_person_id = other_person + self.assertEqual(order.person_id, other_person) + + def test_dayroute_uses_worker_local_date(self): + tuesday_day = self.env.ref("fieldservice_route.fsm_route_day_1") + self.fsm_route_id.day_ids = [(6, 0, [tuesday_day.id])] + self.test_person.partner_id.tz = "America/Toronto" + route_date = self.date.date() + while route_date.weekday() != 1: + route_date += timedelta(days=1) + local_start = timezone("America/Toronto").localize( + datetime.combine( + route_date, datetime.min.time().replace(hour=23, minute=30) + ) + ) + utc_start = local_start.astimezone(utc).replace(tzinfo=None) + self.assertNotEqual(utc_start.date(), route_date) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": utc_start, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order.dayroute_id.date, route_date) + + def test_create_with_dayroute_syncs_person_and_schedule(self): + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "dayroute_id": dayroute.id, + } + ) + self.assertEqual(order.dayroute_id, dayroute) + self.assertEqual(order.person_id, dayroute.person_id) + self.assertEqual(order.scheduled_date_start, dayroute.date_start_planned) + + def test_create_with_mismatched_dayroute_raises(self): + other_route = self.fsm_route_obj.create( + { + "name": "Create Mismatch Route", + "max_order": 10, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": other_route.id, + "date": self.date.date(), + } + ) + with self.assertRaises(ValidationError): + self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "dayroute_id": dayroute.id, + } + ) + + def test_write_dayroute_overrides_conflicting_person_and_date(self): + self.test_person.partner_id.tz = "UTC" + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date.replace(hour=10, minute=0), + "person_id": self.test_person.id, + } + ) + other_person = self.env["fsm.person"].create({"name": "Conflicting Worker"}) + other_person.partner_id.tz = "UTC" + new_date = self.date.date() + timedelta(days=7) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + new_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": new_date, + } + ) + conflicting_start = self.date.replace(hour=15, minute=0) + order.write( + { + "dayroute_id": new_dayroute.id, + "person_id": other_person.id, + "scheduled_date_start": conflicting_start, + } + ) + self.assertEqual(order.dayroute_id, new_dayroute) + self.assertEqual(order.person_id, new_dayroute.person_id) + self.assertNotEqual(order.person_id, other_person) + local_date = order._local_date_from_scheduled_start( + order.scheduled_date_start, + person=order.person_id, + route=order.fsm_route_id, + ) + self.assertEqual(local_date, new_dayroute.date) + self.assertEqual(order.scheduled_date_start.hour, 15) + + def test_dayroute_move_preserves_local_time_across_dst(self): + self.test_person.partner_id.tz = "America/Toronto" + tzinfo = timezone("America/Toronto") + july_date = datetime(2026, 7, 14).date() + january_date = datetime(2026, 1, 13).date() + local_start = tzinfo.localize( + datetime.combine(july_date, datetime.min.time().replace(hour=10)) + ) + utc_start = local_start.astimezone(utc).replace(tzinfo=None) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": utc_start, + "scheduled_duration": 2.0, + "person_id": self.test_person.id, + } + ) + january_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": january_date, + } + ) + order.write({"dayroute_id": january_dayroute.id}) + local_after = utc.localize(order.scheduled_date_start).astimezone(tzinfo) + self.assertEqual(local_after.date(), january_date) + self.assertEqual(local_after.hour, 10) + self.assertEqual(order.scheduled_duration, 2.0) + local_end = utc.localize(order.scheduled_date_end).astimezone(tzinfo) + self.assertEqual(local_end.hour, 12) + + def test_reject_routed_order_on_routeless_dayroute(self): + routeless_dayroute = self.env["fsm.route.dayroute"].create( + { + "date": self.date.date(), + "person_id": self.test_person.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + with self.assertRaises(ValidationError): + order.write({"dayroute_id": routeless_dayroute.id}) + + def test_reject_routeless_order_on_routed_dayroute(self): + routeless_location = self.env["fsm.location"].create( + { + "name": "Routeless Order Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + } + ) + routed_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + with self.assertRaises(ValidationError): + self.env["fsm.order"].create( + { + "location_id": routeless_location.id, + "dayroute_id": routed_dayroute.id, + } + ) + + def test_route_worker_change_reassigns_dayroute_by_timezone(self): + self.fsm_route_id.day_ids = [(6, 0, self.days)] + self.test_person.partner_id.tz = "America/Toronto" + # Tuesday 00:30 Toronto == Monday 21:30 Los Angeles + tuesday = self.date.date() + while tuesday.weekday() != 1: + tuesday += timedelta(days=1) + local_start = timezone("America/Toronto").localize( + datetime.combine(tuesday, datetime.min.time().replace(hour=0, minute=30)) + ) + utc_start = local_start.astimezone(utc).replace(tzinfo=None) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": utc_start, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order.dayroute_id.date, tuesday) + la_person = self.env["fsm.person"].create({"name": "LA Worker"}) + la_person.partner_id.tz = "America/Los_Angeles" + self.fsm_route_id.fsm_person_id = la_person + self.assertEqual(order.person_id, la_person) + la_date = order._local_date_from_scheduled_start( + order.scheduled_date_start, + person=la_person, + route=self.fsm_route_id, + ) + self.assertEqual(la_date, tuesday - timedelta(days=1)) + self.assertEqual(order.dayroute_id.date, la_date) + self.assertEqual(order.dayroute_id.person_id, la_person) + + def test_write_dayroute_with_duration_keeps_positive_end(self): + self.test_person.partner_id.tz = "UTC" + start = datetime(2026, 1, 15, 9, 0, 0) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": start, + "scheduled_duration": 2.0, + "person_id": self.test_person.id, + } + ) + new_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": datetime(2026, 1, 22).date(), + } + ) + order.write( + { + "dayroute_id": new_dayroute.id, + "scheduled_duration": 3.0, + } + ) + self.assertEqual(order.scheduled_date_start.date(), new_dayroute.date) + self.assertEqual(order.scheduled_duration, 3.0) + self.assertEqual( + order.scheduled_date_end, order.scheduled_date_start + timedelta(hours=3) + ) + self.assertGreater(order.scheduled_date_end, order.scheduled_date_start) + + def test_write_dayroute_keeps_end_aligned_with_duration(self): + self.test_person.partner_id.tz = "UTC" + start = datetime(2026, 1, 15, 9, 0, 0) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": start, + "scheduled_duration": 2.0, + "person_id": self.test_person.id, + } + ) + self.assertEqual(order.scheduled_date_end, start + timedelta(hours=2)) + new_dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": datetime(2026, 1, 22).date(), + } + ) + order.write({"dayroute_id": new_dayroute.id}) + self.assertEqual(order.scheduled_date_start.hour, 9) + self.assertEqual(order.scheduled_duration, 2.0) + self.assertEqual( + order.scheduled_date_end, + order.scheduled_date_start + timedelta(hours=2), + ) + + def test_write_duration_on_full_dayroute_keeps_same_dayroute(self): + route = self.fsm_route_obj.create( + { + "name": "Full Capacity Route", + "max_order": 1, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + location = self.env["fsm.location"].create( + { + "name": "Full Capacity Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": location.id, + "scheduled_date_start": self.date, + "scheduled_duration": 1.0, + "person_id": self.test_person.id, + } + ) + dayroute = order.dayroute_id + self.assertEqual(dayroute.order_remaining, 0) + order.write({"scheduled_duration": 2.0}) + self.assertEqual(order.dayroute_id, dayroute) + self.assertEqual(order.scheduled_duration, 2.0) + self.assertTrue(dayroute.exists()) + + def test_fsm_user_can_create_dayroute_when_rescheduling(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + } + ) + fsm_user = self.env["res.users"].create( + { + "name": "FSM Route User", + "login": "fsm_route_dayroute_user", + "group_ids": [(6, 0, [self.env.ref("fieldservice.group_fsm_user").id])], + } + ) + order.with_user(fsm_user).write( + { + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertTrue(order.dayroute_id) + self.assertEqual(order.dayroute_id.person_id, self.test_person) + + def test_create_with_dayroute_start_and_end_keeps_duration(self): + dayroute = self.env["fsm.route.dayroute"].create( + { + "route_id": self.fsm_route_id.id, + "date": self.date.date(), + } + ) + start = datetime.combine(self.date.date(), datetime.min.time().replace(hour=9)) + end = start + timedelta(hours=2) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "dayroute_id": dayroute.id, + "scheduled_date_start": start, + "scheduled_date_end": end, + } + ) + self.assertEqual(order.dayroute_id, dayroute) + self.assertEqual(order.scheduled_duration, 2.0) + self.assertEqual( + order.scheduled_date_end, order.scheduled_date_start + timedelta(hours=2) + ) + self.assertNotEqual(order.scheduled_date_end, order.scheduled_date_start) + + def test_staffing_unstaffed_route_assigns_dayroute(self): + route = self.fsm_route_obj.create( + { + "name": "Unstaffed Then Staffed", + "max_order": 10, + "day_ids": [(6, 0, self.days)], + } + ) + location = self.env["fsm.location"].create( + { + "name": "Unstaffed Route Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": location.id, + "scheduled_date_start": self.date, + } + ) + self.assertFalse(order.dayroute_id) + route.fsm_person_id = self.test_person + self.assertEqual(order.person_id, self.test_person) + self.assertTrue(order.dayroute_id) + self.assertEqual(order.dayroute_id.person_id, self.test_person) + self.assertEqual( + order.dayroute_id.date, + order._local_date_from_scheduled_start( + order.scheduled_date_start, + person=order.person_id, + route=route, + ), + ) + + def test_write_person_false_clears_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + self.assertTrue(old_dayroute) + order.write({"person_id": False}) + self.assertFalse(order.person_id) + self.assertFalse(order.dayroute_id) + self.assertFalse(old_dayroute.exists()) + + def test_fsm_user_can_reschedule_existing_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + fsm_user = self.env["res.users"].create( + { + "name": "FSM Reschedule User", + "login": "fsm_route_reschedule_user", + "group_ids": [(6, 0, [self.env.ref("fieldservice.group_fsm_user").id])], + } + ) + new_date = self.date + timedelta(days=1) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + order.with_user(fsm_user).write({"scheduled_date_start": new_date}) + self.assertTrue(order.dayroute_id) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertEqual(order.dayroute_id.date, new_date.date()) + self.assertFalse(old_dayroute.exists()) + + def test_unstaffing_route_clears_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + self.fsm_route_id.fsm_person_id = False + self.assertFalse(order.person_id) + self.assertFalse(order.dayroute_id) + self.assertFalse(old_dayroute.exists()) + + def test_location_route_change_reassigns_dayroute(self): + other_person = self.env["fsm.person"].create({"name": "Other Route Worker"}) + other_route = self.fsm_route_obj.create( + { + "name": "Location Change Target Route", + "max_order": 10, + "fsm_person_id": other_person.id, + "day_ids": [(6, 0, self.days)], + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + self.assertEqual(old_dayroute.route_id, self.fsm_route_id) + self.test_location.fsm_route_id = other_route + self.assertEqual(order.fsm_route_id, other_route) + self.assertEqual(order.person_id, other_person) + self.assertTrue(order.dayroute_id) + self.assertEqual(order.dayroute_id.route_id, other_route) + self.assertEqual(order.dayroute_id.person_id, other_person) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_write_location_to_unstaffed_route_clears_dayroute(self): + unstaffed_route = self.fsm_route_obj.create( + { + "name": "Unstaffed Target Route", + "max_order": 10, + "day_ids": [(6, 0, self.days)], + } + ) + unstaffed_location = self.env["fsm.location"].create( + { + "name": "Unstaffed Target Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": unstaffed_route.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + self.assertTrue(old_dayroute) + order.write({"location_id": unstaffed_location.id}) + self.assertEqual(order.fsm_route_id, unstaffed_route) + self.assertFalse(order.person_id) + self.assertFalse(order.dayroute_id) + self.assertFalse(old_dayroute.exists()) + stale = self.env["fsm.route.dayroute"].search( + [ + ("route_id", "=", unstaffed_route.id), + ("person_id", "=", self.test_person.id), + ] + ) + self.assertFalse(stale) + + def test_reschedule_keeps_manually_assigned_worker(self): + other_person = self.env["fsm.person"].create({"name": "Manual Assignee"}) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + order.write({"person_id": other_person.id}) + self.assertEqual(order.person_id, other_person) + self.assertEqual(order.dayroute_id.person_id, other_person) + old_dayroute = order.dayroute_id + new_date = self.date + timedelta(days=1) + while new_date.weekday() > 4: + new_date += timedelta(days=1) + order.write({"scheduled_date_start": new_date}) + self.assertEqual(order.person_id, other_person) + self.assertTrue(order.dayroute_id) + self.assertEqual(order.dayroute_id.person_id, other_person) + self.assertEqual(order.dayroute_id.date, new_date.date()) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertNotEqual( + order.dayroute_id.person_id, self.fsm_route_id.fsm_person_id + ) + + def test_get_person_id_keeps_worker_when_moving_to_routeless(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + routeless_location = self.env["fsm.location"].create( + { + "name": "Routeless Move Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + } + ) + person_id = order._get_person_id_for_dayroute( + {"location_id": routeless_location.id}, False + ) + self.assertEqual(person_id, self.test_person.id) + + def test_get_person_id_from_route_without_order_person(self): + order = self.env["fsm.order"].new({}) + self.assertFalse(order.person_id) + person_id = order._get_person_id_for_dayroute({}, self.fsm_route_id.id) + self.assertEqual(person_id, self.test_person.id) + + def test_get_person_id_from_order_route_when_no_route_id(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + order.person_id = False + person_id = order._get_person_id_for_dayroute({}, False) + self.assertEqual(person_id, self.fsm_route_id.fsm_person_id.id) + + def test_local_date_from_scheduled_start_without_start(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + self.assertFalse(order._local_date_from_scheduled_start(False)) + self.assertFalse(order._local_date_from_scheduled_start(None)) + + def test_duration_for_schedule_sync_from_start_and_end(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + start = self.date.replace(hour=9, minute=0, second=0, microsecond=0) + end = start + timedelta(hours=2) + duration = order._duration_for_schedule_sync( + { + "scheduled_date_start": start, + "scheduled_date_end": end, + } + ) + self.assertEqual(duration, 2.0) + + def test_sync_scheduled_end_without_start_returns_vals(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + vals = {"scheduled_duration": 1.0} + self.assertEqual(order._sync_scheduled_end_from_start(vals), vals) + + def test_reassign_dayroute_skips_unscheduled_order(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + } + ) + self.assertFalse(order.scheduled_date_start) + self.assertFalse(order.dayroute_id) + order._reassign_dayroute_for_person_timezone() + self.assertFalse(order.dayroute_id) + + def test_reassign_dayroute_keeps_matching_dayroute(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + dayroute = order.dayroute_id + order._reassign_dayroute_for_person_timezone() + self.assertEqual(order.dayroute_id, dayroute) + + def test_prepare_vals_from_dayroute_without_person_or_date(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + dayroute = self.env["fsm.route.dayroute"].new( + { + "route_id": self.fsm_route_id.id, + "person_id": False, + "date": False, + } + ) + vals = order._prepare_vals_from_dayroute(dayroute, {"description": "x"}) + self.assertEqual(vals, {"description": "x"}) + self.assertNotIn("person_id", vals) + self.assertNotIn("scheduled_date_start", vals) + + def test_write_location_to_routeless_keeps_worker_dayroute(self): + routeless_location = self.env["fsm.location"].create( + { + "name": "Routeless Write Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + old_dayroute = order.dayroute_id + order.write({"location_id": routeless_location.id}) + self.assertFalse(order.fsm_route_id) + self.assertEqual(order.person_id, self.test_person) + self.assertTrue(order.dayroute_id) + self.assertEqual(order.dayroute_id.person_id, self.test_person) + self.assertFalse(order.dayroute_id.route_id) + self.assertNotEqual(order.dayroute_id, old_dayroute) + self.assertFalse(old_dayroute.exists()) + + def test_tz_name_falls_back_to_utc(self): + order = self.env["fsm.order"].new({}) + person = self.env["fsm.person"].create({"name": "No TZ Worker"}) + person.partner_id.tz = False + old_tz = self.env.user.tz + self.env.user.tz = False + try: + self.assertEqual( + order._tz_name_for_route_day( + person=person, route=self.env["fsm.route"] + ), + "UTC", + ) + finally: + self.env.user.tz = old_tz + + def test_capture_duration_skips_when_duration_present(self): + order = self.env["fsm.order"].new({"location_id": self.test_location.id}) + start = self.date.replace(hour=9, minute=0, second=0, microsecond=0) + vals = { + "scheduled_duration": 4.0, + "scheduled_date_start": start, + "scheduled_date_end": start + timedelta(hours=1), + } + result = order._capture_duration_from_start_end(vals) + self.assertEqual(result["scheduled_duration"], 4.0) + + def test_route_id_changing_false_when_same_route(self): + order = self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": self.date, + "person_id": self.test_person.id, + } + ) + self.assertFalse( + order._route_id_changing( + {"location_id": self.test_location.id}, self.fsm_route_id.id + ) + ) diff --git a/fieldservice_route/tests/test_fsm_route.py b/fieldservice_route/tests/test_fsm_route.py new file mode 100644 index 0000000000..81c46bba8f --- /dev/null +++ b/fieldservice_route/tests/test_fsm_route.py @@ -0,0 +1,136 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.exceptions import ValidationError + +from odoo.addons.fieldservice.tests.test_fsm_common import FSMCommon + + +class TestFSMRoute(FSMCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.Route = cls.env["fsm.route"] + cls.DayRoute = cls.env["fsm.route.dayroute"] + cls.monday = cls.env.ref("fieldservice_route.fsm_route_day_0") + cls.tuesday = cls.env.ref("fieldservice_route.fsm_route_day_1") + cls.route = cls.Route.create( + { + "name": "Weekday Route", + "max_order": 2, + "fsm_person_id": cls.test_person.id, + "day_ids": [(6, 0, [cls.monday.id, cls.tuesday.id])], + } + ) + + def _next_weekday(self, weekday): + date = datetime.now().date() + while date.weekday() != weekday: + date += timedelta(days=1) + return date + + def test_run_on_scheduled_day(self): + monday = self._next_weekday(0) + self.assertTrue(self.route.run_on(monday)) + + def test_run_on_non_scheduled_day(self): + wednesday = self._next_weekday(2) + self.assertFalse(self.route.run_on(wednesday)) + + def test_run_on_no_date(self): + self.assertFalse(self.route.run_on(False)) + + def test_dayroute_sequence_name(self): + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + } + ) + self.assertTrue(dayroute.name.startswith("SR")) + + def test_dayroute_person_from_route(self): + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + } + ) + self.assertEqual(dayroute.person_id, self.route.fsm_person_id) + + def test_dayroute_order_count(self): + route_date = self._next_weekday(0) + self.test_location.fsm_route_id = self.route + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + self.assertEqual(dayroute.order_count, 0) + self.assertEqual(dayroute.order_remaining, self.route.max_order) + self.env["fsm.order"].create( + { + "location_id": self.test_location.id, + "scheduled_date_start": datetime.combine( + route_date, datetime.min.time() + ), + "person_id": self.test_person.id, + "dayroute_id": dayroute.id, + } + ) + dayroute.invalidate_recordset() + self.assertEqual(dayroute.order_count, 1) + self.assertEqual(dayroute.order_remaining, 1) + + def test_dayroute_check_day_constraint(self): + wednesday = self._next_weekday(2) + with self.assertRaises(ValidationError): + self.DayRoute.create( + { + "route_id": self.route.id, + "date": wednesday, + } + ) + + def test_dayroute_check_capacity_constraint(self): + route = self.Route.create( + { + "name": "Single Order Route", + "max_order": 1, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, [self.monday.id])], + } + ) + route_date = self._next_weekday(0) + location = self.env["fsm.location"].create( + { + "name": "Capacity Route Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + dayroute = self.DayRoute.create( + { + "route_id": route.id, + "date": route_date, + } + ) + self.env["fsm.order"].create( + { + "location_id": location.id, + "dayroute_id": dayroute.id, + } + ) + with self.assertRaises(ValidationError): + self.env["fsm.order"].create( + { + "location_id": location.id, + "dayroute_id": dayroute.id, + } + ) diff --git a/fieldservice_route/tests/test_fsm_route_dayroute.py b/fieldservice_route/tests/test_fsm_route_dayroute.py new file mode 100644 index 0000000000..b0a5b5f078 --- /dev/null +++ b/fieldservice_route/tests/test_fsm_route_dayroute.py @@ -0,0 +1,561 @@ +# Copyright (C) 2026 Gray Matter Logic +# Copyright (C) 2019 Serpent consulting Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, time, timedelta +from unittest.mock import patch + +from pytz import timezone, utc + +from odoo.exceptions import AccessError, ValidationError +from odoo.tools import mute_logger + +from odoo.addons.fieldservice.tests.test_fsm_common import FSMCommon + + +class TestFSMRouteDayRoute(FSMCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.DayRoute = cls.env["fsm.route.dayroute"] + cls.Route = cls.env["fsm.route"] + cls.monday = cls.env.ref("fieldservice_route.fsm_route_day_0") + cls.route = cls.Route.create( + { + "name": "Worker Route", + "max_order": 5, + "fsm_person_id": cls.test_person.id, + "day_ids": [(6, 0, [cls.monday.id])], + } + ) + + def _next_weekday(self, weekday): + date = datetime.now().date() + while date.weekday() != weekday: + date += timedelta(days=1) + return date + + def test_default_team_required(self): + with mute_logger("odoo.models.unlink"): + self.env["fsm.team"].search([]).unlink() + with self.assertRaisesRegex( + ValidationError, "You must create a FSM team first." + ): + self.DayRoute.create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + } + ) + + def test_person_without_route_worker(self): + route = self.Route.create( + { + "name": "Unstaffed Route", + "day_ids": [(6, 0, [self.monday.id])], + } + ) + dayroute = self.DayRoute.create( + { + "route_id": route.id, + "date": self._next_weekday(0), + } + ) + self.assertFalse(dayroute.person_id) + + def test_compute_date_start_planned_without_date(self): + dayroute = self.DayRoute.new({"route_id": self.route.id}) + dayroute._compute_date_start_planned() + self.assertFalse(dayroute.date_start_planned) + + def test_create_without_route_id_with_person(self): + route_date = self._next_weekday(0) + dayroute = self.DayRoute.create( + { + "date": route_date, + "person_id": self.test_person.id, + } + ) + self.assertTrue(dayroute.date_start_planned) + + def test_planned_start_uses_route_worker(self): + route_date = self._next_weekday(0) + planned = self.DayRoute._planned_start_from_date( + route_date, + route=self.route, + ) + self.assertTrue(planned) + + def test_planned_start_company_calendar_timezone(self): + route_date = self._next_weekday(0) + self.test_person.partner_id.tz = False + self.test_person.calendar_id = False + self.env.company.resource_calendar_id.tz = "UTC" + planned = self.DayRoute._planned_start_from_date( + route_date, + person=self.test_person, + route=self.route, + ) + self.assertTrue(planned) + + def test_planned_start_user_timezone_fallback(self): + route_date = self._next_weekday(0) + self.test_person.partner_id.tz = False + self.test_person.calendar_id = False + calendar = self.env.company.resource_calendar_id + with ( + patch.object(type(calendar), "_get_closest_work_time", return_value=None), + patch.object(type(calendar), "tz", False), + patch.object(type(self.env.user), "tz", False), + ): + planned = self.DayRoute._planned_start_from_date( + route_date, + person=self.test_person, + route=self.route, + ) + self.assertTrue(planned) + + def test_check_day_skipped_without_route_or_date(self): + dayroute = self.DayRoute.new({}) + dayroute.check_day() + + def test_create_without_route_id(self): + route_date = self._next_weekday(0) + dayroute = self.DayRoute.create({"date": route_date}) + self.assertTrue(dayroute.date_start_planned) + + def test_compute_date_start_clears_without_date(self): + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + } + ) + dayroute.write({"date": False}) + self.assertFalse(dayroute.date_start_planned) + + def test_planned_start_without_work_interval(self): + route_date = self._next_weekday(0) + calendar = self.env["resource.calendar"].create( + { + "name": "No Attendance", + "tz": "UTC", + "attendance_ids": [], + } + ) + self.test_person.calendar_id = calendar + with patch.object(type(calendar), "_get_closest_work_time", return_value=None): + planned = self.DayRoute._planned_start_from_date( + route_date, + person=self.test_person, + route=self.route, + ) + self.assertTrue(planned) + + def test_dayroute_create_with_date_start_planned(self): + route_date = self._next_weekday(0) + planned = self.DayRoute._planned_start_from_date( + route_date, + route=self.route, + ) + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + "date_start_planned": planned, + } + ) + self.assertEqual(dayroute.date_start_planned, planned) + + def test_compute_date_start_uses_dayroute_person(self): + other_person = self.env["fsm.person"].create({"name": "Other Worker"}) + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + "person_id": other_person.id, + } + ) + dayroute.invalidate_recordset(["date_start_planned"]) + dayroute._compute_date_start_planned() + self.assertTrue(dayroute.date_start_planned) + + def test_portal_user_sees_only_own_dayroutes(self): + other_person = self.env["fsm.person"].create({"name": "Other Worker"}) + route_date = self._next_weekday(0) + own_dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + other_route = self.Route.create( + { + "name": "Other Worker Route", + "max_order": 5, + "fsm_person_id": other_person.id, + "day_ids": [(6, 0, [self.monday.id])], + } + ) + other_dayroute = self.DayRoute.create( + { + "route_id": other_route.id, + "date": route_date, + } + ) + portal_user = self.env["res.users"].create( + { + "name": self.test_person.name, + "login": "portal_route_worker", + "partner_id": self.test_person.partner_id.id, + "group_ids": [(6, 0, [self.env.ref("base.group_portal").id])], + } + ) + portal_dayroutes = self.DayRoute.with_user(portal_user).search([]) + self.assertIn(own_dayroute, portal_dayroutes) + self.assertNotIn(other_dayroute, portal_dayroutes) + + def test_portal_user_cannot_write_dayroutes(self): + route_date = self._next_weekday(0) + own_dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + portal_user = self.env["res.users"].create( + { + "name": self.test_person.name, + "login": "portal_route_worker_write", + "partner_id": self.test_person.partner_id.id, + "group_ids": [(6, 0, [self.env.ref("base.group_portal").id])], + } + ) + with self.assertRaises(AccessError): + own_dayroute.with_user(portal_user).write({"name": "Portal edit"}) + + def test_planned_start_recomputes_on_two_week_calendar(self): + route_date = self._next_weekday(0) + calendar = self.env["resource.calendar"].create( + { + "name": "Two Week Shift", + "tz": "UTC", + "two_weeks_calendar": True, + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Week 1 Monday", + "dayofweek": "0", + "week_type": "0", + "hour_from": 7.0, + "hour_to": 15.0, + "day_period": "morning", + }, + ) + ], + } + ) + self.test_person.calendar_id = calendar + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + before = dayroute.date_start_planned + calendar.attendance_ids.write({"week_type": "1"}) + self.assertNotEqual(dayroute.date_start_planned, before) + + def test_planned_start_uses_team_company_calendar(self): + route_date = self._next_weekday(0) + other_company = self.env["res.company"].create({"name": "Other Company"}) + other_calendar = self.env["resource.calendar"].create( + { + "name": "Other Company Calendar", + "tz": "UTC", + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Other Shift", + "dayofweek": str(route_date.weekday()), + "hour_from": 5.0, + "hour_to": 13.0, + "day_period": "morning", + }, + ) + ], + } + ) + other_company.resource_calendar_id = other_calendar + other_team = self.env["fsm.team"].create( + { + "name": "Other Company Team", + "company_id": other_company.id, + } + ) + self.test_person.calendar_id = False + dayroute = self.DayRoute.with_company(other_company).create( + { + "route_id": self.route.id, + "date": route_date, + "team_id": other_team.id, + } + ) + planned = dayroute.date_start_planned + self.assertTrue(planned) + self.assertEqual(planned.hour, 5) + + def test_create_sequence_fallback(self): + route_date = self._next_weekday(0) + with patch.object( + type(self.env["ir.sequence"]), + "next_by_code", + return_value=False, + ): + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + self.assertEqual(dayroute.name, "New") + + def test_order_remaining_recomputes_on_max_order_change(self): + route_date = self._next_weekday(0) + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + self.assertEqual(dayroute.order_remaining, 5) + self.route.max_order = 3 + self.assertEqual(dayroute.order_remaining, 3) + + def test_default_team_uses_active_company(self): + other_company = self.env["res.company"].create({"name": "Active Company"}) + self.env["fsm.team"].search( + [("company_id", "in", (self.env.company.id, False))] + ).write({"sequence": 100}) + other_team = self.env["fsm.team"].create( + { + "name": "Active Company Team", + "company_id": other_company.id, + "sequence": 1, + } + ) + dayroute = self.DayRoute.with_company(other_company).create( + { + "route_id": self.route.id, + "date": self._next_weekday(0), + } + ) + self.assertEqual(dayroute.team_id, other_team) + + def test_create_without_team_uses_default_team_calendar(self): + route_date = self._next_weekday(0) + other_company = self.env["res.company"].create({"name": "Default Team Co"}) + other_calendar = self.env["resource.calendar"].create( + { + "name": "Default Team Calendar", + "tz": "UTC", + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Early", + "dayofweek": str(route_date.weekday()), + "hour_from": 4.0, + "hour_to": 12.0, + "day_period": "morning", + }, + ) + ], + } + ) + other_company.resource_calendar_id = other_calendar + self.env["fsm.team"].search( + [("company_id", "in", (self.env.company.id, False))] + ).write({"sequence": 100}) + other_team = self.env["fsm.team"].create( + { + "name": "Default Team Calendar Team", + "company_id": other_company.id, + "sequence": 1, + } + ) + self.test_person.calendar_id = False + dayroute = self.DayRoute.with_company(other_company).create( + { + "route_id": self.route.id, + "date": route_date, + } + ) + self.assertEqual(dayroute.team_id, other_team) + self.assertEqual(dayroute.date_start_planned.hour, 4) + + def test_portal_user_cannot_read_ir_sequence(self): + portal_user = self.env["res.users"].create( + { + "name": self.test_person.name, + "login": "portal_route_sequence", + "partner_id": self.test_person.partner_id.id, + "group_ids": [(6, 0, [self.env.ref("base.group_portal").id])], + } + ) + sequence = self.env.ref("fieldservice_route.seq_fsm_route") + with self.assertRaises(AccessError): + sequence.with_user(portal_user).read(["name"]) + + def test_create_with_custom_name_skips_sequence(self): + route_date = self._next_weekday(0) + dayroute = self.DayRoute.create( + { + "name": "Custom Day Route", + "route_id": self.route.id, + "date": route_date, + } + ) + self.assertEqual(dayroute.name, "Custom Day Route") + + def test_create_with_explicit_team_id(self): + route_date = self._next_weekday(0) + team = self.env["fsm.team"].search([], limit=1) + dayroute = self.DayRoute.create( + { + "route_id": self.route.id, + "date": route_date, + "team_id": team.id, + } + ) + self.assertEqual(dayroute.team_id, team) + self.assertTrue(dayroute.date_start_planned) + + def test_default_stage_id(self): + stage = self.DayRoute._default_stage_id() + self.assertTrue(stage) + self.assertEqual(stage.stage_type, "route") + + def test_planned_start_without_calendar(self): + route_date = self._next_weekday(0) + other_company = self.env["res.company"].create({"name": "No Calendar Co"}) + other_company.resource_calendar_id = False + other_team = self.env["fsm.team"].create( + { + "name": "No Calendar Team", + "company_id": other_company.id, + } + ) + person = self.env["fsm.person"].create({"name": "No Calendar Worker"}) + person.calendar_id = False + person.partner_id.tz = "UTC" + planned = self.DayRoute._planned_start_from_date( + route_date, + person=person, + team=other_team, + ) + self.assertEqual(planned.hour, 8) + + def test_check_capacity_allows_equal_max_order(self): + route = self.Route.create( + { + "name": "Exact Capacity", + "max_order": 1, + "fsm_person_id": self.test_person.id, + "day_ids": [(6, 0, [self.monday.id])], + } + ) + location = self.env["fsm.location"].create( + { + "name": "Exact Capacity Location", + "partner_id": self.test_loc_partner.id, + "owner_id": self.test_loc_partner.id, + "fsm_route_id": route.id, + } + ) + dayroute = self.DayRoute.create( + { + "route_id": route.id, + "date": self._next_weekday(0), + } + ) + self.env["fsm.order"].create( + { + "location_id": location.id, + "dayroute_id": dayroute.id, + } + ) + dayroute.invalidate_recordset() + self.assertEqual(dayroute.order_count, 1) + dayroute.check_capacity() + + def _assert_planned_start_search_range_uses_local_midnights( + self, route_date, tz_name + ): + tzinfo = timezone(tz_name) + expected_start = tzinfo.localize(datetime.combine(route_date, time.min)) + expected_end = tzinfo.localize( + datetime.combine(route_date + timedelta(days=1), time.min) + ) + calendar = self.env["resource.calendar"].create( + { + "name": f"DST {tz_name} {route_date}", + "tz": tz_name, + "attendance_ids": [ + ( + 0, + 0, + { + "name": "Shift", + "dayofweek": str(route_date.weekday()), + "hour_from": 9.0, + "hour_to": 17.0, + "day_period": "morning", + }, + ) + ], + } + ) + self.test_person.partner_id.tz = tz_name + self.test_person.calendar_id = calendar + captured = {} + + work_time = tzinfo.localize(datetime.combine(route_date, time(9, 0))) + + def _capture_closest_work_time( + _self, start, match_end=False, search_range=None, compute_leaves=True + ): + captured["search_range"] = search_range + return work_time + + with patch.object( + type(calendar), + "_get_closest_work_time", + autospec=True, + side_effect=_capture_closest_work_time, + ): + planned = self.DayRoute._planned_start_from_date( + route_date, + person=self.test_person, + route=self.route, + ) + self.assertEqual(captured["search_range"], (expected_start, expected_end)) + self.assertEqual(planned, work_time.astimezone(utc).replace(tzinfo=None)) + + def test_planned_start_search_range_spring_forward_dst(self): + # US/Eastern spring forward 2026-03-08 (EST -> EDT). + self._assert_planned_start_search_range_uses_local_midnights( + datetime(2026, 3, 8).date(), "US/Eastern" + ) + + def test_planned_start_search_range_fall_back_dst(self): + # US/Eastern fall back 2026-11-01 (EDT -> EST). + self._assert_planned_start_search_range_uses_local_midnights( + datetime(2026, 11, 1).date(), "US/Eastern" + ) diff --git a/fieldservice_route/views/fsm_location.xml b/fieldservice_route/views/fsm_location.xml new file mode 100644 index 0000000000..a6a6e1409e --- /dev/null +++ b/fieldservice_route/views/fsm_location.xml @@ -0,0 +1,12 @@ + + + fsm.location.form + fsm.location + + + + + + + + diff --git a/fieldservice_route/views/fsm_order.xml b/fieldservice_route/views/fsm_order.xml new file mode 100644 index 0000000000..5720abb19a --- /dev/null +++ b/fieldservice_route/views/fsm_order.xml @@ -0,0 +1,15 @@ + + + fsm.order.form + fsm.order + + + + + + + + + + + diff --git a/fieldservice_route/views/fsm_route.xml b/fieldservice_route/views/fsm_route.xml new file mode 100644 index 0000000000..9cc65d8eed --- /dev/null +++ b/fieldservice_route/views/fsm_route.xml @@ -0,0 +1,61 @@ + + + fsm.route.list + fsm.route + + + + + + + + + + + fsm.route.form + fsm.route + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + + Routes + fsm.route + list,form + +

+ Add Routes here. +

+
+
+
diff --git a/fieldservice_route/views/fsm_route_day.xml b/fieldservice_route/views/fsm_route_day.xml new file mode 100644 index 0000000000..59262dc966 --- /dev/null +++ b/fieldservice_route/views/fsm_route_day.xml @@ -0,0 +1,36 @@ + + + Route Day + fsm.route.day + + + + + + + + + Route Day + fsm.route.day + +
+ + + + + +
+
+
+ + + Route Day + fsm.route.day + list,form + +

+ Create a Route Day. +

+
+
+
diff --git a/fieldservice_route/views/fsm_route_dayroute.xml b/fieldservice_route/views/fsm_route_dayroute.xml new file mode 100644 index 0000000000..4e89400102 --- /dev/null +++ b/fieldservice_route/views/fsm_route_dayroute.xml @@ -0,0 +1,176 @@ + + + fsm.route.dayroute.list + fsm.route.dayroute + + + + + + + + + + + + + fsm.route.dayroute.form + fsm.route.dayroute + +
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +