Compare commits
51 commits
18.0
...
bemade_fsm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f21f26dbf | ||
|
|
741171a210 | ||
|
|
f12583275b | ||
|
|
22cf91c67c | ||
|
|
05ac09014c | ||
|
|
aef2d80c88 | ||
|
|
7d79d21045 | ||
|
|
b1ca61ce89 | ||
|
|
7b5a7b63c2 | ||
|
|
e845f84ab5 | ||
|
|
f4652057f1 | ||
|
|
dd94e2a1cf | ||
|
|
40b362185d | ||
|
|
26603d5701 | ||
|
|
2e1161c6ad | ||
|
|
e1bf6e18b6 | ||
|
|
e604757617 | ||
|
|
4806b245c8 | ||
|
|
4c7ba22f87 | ||
|
|
28a9e7a8d1 | ||
|
|
a8472ec00d | ||
|
|
31a2cae37c | ||
|
|
f4b8770d5d | ||
|
|
caadbdc342 | ||
|
|
034e3b0cbb | ||
|
|
ce53137871 | ||
|
|
66923c14bf | ||
|
|
2303753f3d | ||
|
|
66cac13616 | ||
|
|
7cdcd2995e | ||
|
|
1f47523515 | ||
|
|
21ba9e640f | ||
|
|
04fb5699ac | ||
|
|
b3aa92afe1 | ||
|
|
4ca7dcd73d | ||
|
|
515246860f | ||
|
|
7541480bfd | ||
|
|
9d3769ae10 | ||
|
|
963fa748c6 | ||
|
|
82b51907a5 | ||
|
|
3e311ead94 | ||
|
|
24a5e6e2b9 | ||
|
|
6159eae798 | ||
|
|
878ccf849b | ||
|
|
565446ee9d | ||
|
|
a4a998e012 | ||
|
|
218e721261 | ||
|
|
05aae321be | ||
|
|
395bd999a0 | ||
|
|
f7520c50b8 | ||
|
|
07b0f992a4 |
42 changed files with 3224 additions and 465 deletions
|
|
@ -20,38 +20,49 @@
|
|||
########################################################################################
|
||||
{
|
||||
'name': 'Improved Field Service Management',
|
||||
'version': '15.0.0.2.0',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Adds functionality necessary for managing field service operations at Durpro.',
|
||||
'description': 'Adds functionality necessary for managing field service operations at Durpro.',
|
||||
'category': 'Services/Field Service',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['project',
|
||||
'depends': ['project_enterprise',
|
||||
'project_forecast',
|
||||
'stock',
|
||||
'sale',
|
||||
'sale_project',
|
||||
'sale_stock',
|
||||
'industry_fsm',
|
||||
'industry_fsm_sale',
|
||||
'sale_planning',
|
||||
'worksheet',
|
||||
'industry_fsm_stock',
|
||||
'industry_fsm_report',
|
||||
'industry_fsm_sale_report',
|
||||
'bemade_partner_root_ancestor',
|
||||
'mail',
|
||||
],
|
||||
'data': ['views/task_template_views.xml',
|
||||
'views/equipment.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/product_views.xml',
|
||||
'views/res_partner.xml',
|
||||
'views/menus.xml',
|
||||
'views/task_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
],
|
||||
'data': [
|
||||
'data/fsm_data.xml',
|
||||
'views/task_template_views.xml',
|
||||
'views/equipment.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/product_views.xml',
|
||||
'views/res_partner.xml',
|
||||
'views/menus.xml',
|
||||
'views/task_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'reports/worksheet_custom_report_templates.xml',
|
||||
'reports/worksheet_custom_reports.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_tests': [
|
||||
'bemade_fsm/static/tests/tours/task_template_tour.js',
|
||||
'bemade_fsm/static/tests/tours/equipment_tour.js',
|
||||
'bemade_fsm/static/tests/tours/sale_order_tour.js',
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
'bemade_fsm/static/src/scss/bemade_fsm.scss'
|
||||
]
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
|
|
|
|||
48
bemade_fsm/data/fsm_data.xml
Normal file
48
bemade_fsm/data/fsm_data.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="planning_project_stage_waiting_parts" model="project.task.type">
|
||||
<field name="sequence">2</field>
|
||||
<field name="name">Waiting on Parts</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</record>
|
||||
<record id="planning_project_stage_work_completed" model="project.task.type">
|
||||
<field name="sequence">15</field>
|
||||
<field name="name">Work Executed</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</record>
|
||||
<record id="planning_project_stage_exception" model="project.task.type">
|
||||
<field name="sequence">19</field>
|
||||
<field name="name">Exception</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</record>
|
||||
<!-- Since the Field Service project has no_update="1" we use a workaround here -->
|
||||
<function model="ir.model.data" name="write">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('name', '=', 'fsm_project'), ('module', '=', 'industry_fsm'), ('model', '=', 'project.project')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': False}"/>
|
||||
</function>
|
||||
<record id="industry_fsm.fsm_project" model="project.project">
|
||||
<field name="type_ids"
|
||||
eval="[(4, ref('industry_fsm.planning_project_stage_0')), (4, ref('industry_fsm.planning_project_stage_1')), (4, ref('industry_fsm.planning_project_stage_2')), (4, ref('planning_project_stage_work_completed')), (4, ref('industry_fsm.planning_project_stage_3')), (4, ref('industry_fsm.planning_project_stage_4'))]"/>
|
||||
<field name="allow_subtasks"
|
||||
eval="True"/>
|
||||
</record>
|
||||
<function model="ir.model.data" name="write">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('name', '=', 'fsm_project'), ('module', '=', 'industry_fsm'), ('model', '=', 'project.project')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': True}"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
987
bemade_fsm/i18n/fr.po
Normal file
987
bemade_fsm/i18n/fr.po
Normal file
|
|
@ -0,0 +1,987 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * bemade_fsm
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 15:04+0000\n"
|
||||
"PO-Revision-Date: 2023-08-29 15:04+0000\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: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid ""
|
||||
"<span groups=\"account.group_show_line_subtotals_tax_excluded\">\n"
|
||||
" Amount</span>\n"
|
||||
" <span groups=\"account.group_show_line_subtotals_tax_included\">\n"
|
||||
" Total Price</span>"
|
||||
msgstr ""
|
||||
"<span groups=\"account.group_show_line_subtotals_tax_excluded\">\n"
|
||||
" Prix</span>\n"
|
||||
" <span groups=\"account.group_show_line_subtotals_tax_included\">\n"
|
||||
" Prix</span>"
|
||||
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_signature_block
|
||||
msgid ""
|
||||
"<span style=\" font-size: 10px; color: #fff; text-transform: uppercase; text-align: center; font-weight: bold; line-height: 20px; transform: rotate(45deg); width: 100px; height: auto; display: block; background: green; position: absolute; top: 19px; right: -21px; left: auto; padding: 0;\">\n"
|
||||
" Signed\n"
|
||||
" </span>"
|
||||
msgstr ""
|
||||
"<span style=\" font-size: 10px; color: #fff; text-transform: uppercase; text-align: center; font-weight: bold; line-height: 20px; transform: rotate(45deg); width: 100px; height: auto; display: block; background: green; position: absolute; top: 19px; right: -21px; left: auto; padding: 0;\">\n"
|
||||
" Signé\n"
|
||||
" </span>"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "<span>Disc.%</span>"
|
||||
msgstr "<span>Escompte %</span>"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid ""
|
||||
"<strong class=\"mr16\">Section\n"
|
||||
" Subtotal</strong>"
|
||||
msgstr ""
|
||||
"<strong class=\"mr16\">Section\n"
|
||||
" Sous-total</strong>"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "<strong>Taxes</strong>"
|
||||
msgstr "<strong>Taxes</strong>"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "<strong>Total</strong>"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "<strong>Untaxed amount</strong>"
|
||||
msgstr "<strong>Montant hors taxes</strong>"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_needaction
|
||||
msgid "Action Needed"
|
||||
msgstr "Action requise"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_ids
|
||||
msgid "Activities"
|
||||
msgstr "Activités"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_type_icon
|
||||
msgid "Activity Type Icon"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__tag_ids
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
|
||||
msgid "Application"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__approx_date
|
||||
msgid "Approximate Date"
|
||||
msgstr "Date approximative"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_attachment_count
|
||||
msgid "Attachment Count"
|
||||
msgstr "Nombre de pièces jointes"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,legend_blocked:bemade_fsm.planning_project_stage_exception
|
||||
#: model:project.task.type,legend_blocked:bemade_fsm.planning_project_stage_waiting_parts
|
||||
#: model:project.task.type,legend_blocked:bemade_fsm.planning_project_stage_work_completed
|
||||
msgid "Blocked"
|
||||
msgstr "Bloqué"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__allow_billable
|
||||
msgid "Can be billed"
|
||||
msgstr "Facturable"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_res_partner__is_site_contact
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_res_users__is_site_contact
|
||||
msgid ""
|
||||
"Check if the contact is a site contact, otherwise it is a not in that list"
|
||||
msgstr ""
|
||||
"Vérifier si le contact est un contact du site, sinon ne pas l'inclure dans la liste"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__tag_ids
|
||||
msgid ""
|
||||
"Classify and analyze your equipment categories like: Boiler, Laboratory, "
|
||||
"Waste water, Pure water"
|
||||
msgstr ""
|
||||
"Classifier et analyser vos catégories d'équipements comme : Chaudière, Laboratoire, "
|
||||
"Eaux usées, Eau pure"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.ui.menu,name:bemade_fsm.menu_service_client_equipment
|
||||
msgid "Client Equipment"
|
||||
msgstr "Équipement client"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.ui.menu,name:bemade_fsm.menu_service_client
|
||||
#: model:ir.ui.menu,name:bemade_fsm.menu_service_client_clients
|
||||
msgid "Clients"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__color
|
||||
msgid "Color Index"
|
||||
msgstr "Index de couleur"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
|
||||
msgid "Comments"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__company_id
|
||||
msgid "Company"
|
||||
msgstr "Société"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__is_completed
|
||||
msgid "Completed"
|
||||
msgstr "Complété"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.sale_order_form_inherit
|
||||
msgid "Contacts and Equipment"
|
||||
msgstr "Contacts et équipements"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__create_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__create_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__create_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__create_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__create_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__create_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__create_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__create_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Date de création"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
|
||||
msgid "Customer:"
|
||||
msgstr "Client :"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__assignees
|
||||
msgid "Default Assignees"
|
||||
msgstr "Assigné par défaut"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__customer
|
||||
msgid "Default Customer"
|
||||
msgstr "Client par défaut"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__default_equipment_ids
|
||||
msgid "Default Equipment to Service"
|
||||
msgstr "Équipement à désservir par défaut"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__project
|
||||
msgid "Default Project"
|
||||
msgstr "Projet par défaut"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__tags
|
||||
msgid "Default Tags"
|
||||
msgstr "Étiquettes par défaut"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_project_task_template__customer
|
||||
msgid "Default customer for tasks created from this template."
|
||||
msgstr "Client par défaut pour tâches créées à partir de ce gabarit."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_project_task_template__project
|
||||
msgid "Default project for tasks created from this template."
|
||||
msgstr "Projet par défaut pour tâches créées à partir de ce gabarit."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_project_task_template__tags
|
||||
msgid "Default tags for tasks created from this template."
|
||||
msgstr "Étiquettes par défaut pour tâches créées à partir de ce gabarit."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "Delivered"
|
||||
msgstr "Livré"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__description
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__description
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.fsm_equipment_view_tree
|
||||
msgid "Details"
|
||||
msgstr "Détails"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__display_name
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__display_name
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__display_name
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__display_name
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom pour affichage"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_project_task_template__assignees
|
||||
msgid "Employees assigned to tasks created from this template."
|
||||
msgstr "Employés assignés aux tâches créées à partir de ce gabarit."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__planned_date_end
|
||||
msgid "End Date"
|
||||
msgstr "Date de fin"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.actions.act_window,name:bemade_fsm.action_window_equipment
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.equipment_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.equipment_view_tree
|
||||
msgid "Equipment"
|
||||
msgstr "Équipement"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__summary_equipment_ids
|
||||
msgid "Equipment Being Serviced"
|
||||
msgstr "Équipement désservi"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__equipment_count
|
||||
msgid "Equipment Count"
|
||||
msgstr "Nombre d'équipements"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__complete_name
|
||||
msgid "Equipment Name"
|
||||
msgstr "Nom d'équipement"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
|
||||
msgid "Equipment Serviced"
|
||||
msgstr "Équipement désservi"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.actions.act_window,name:bemade_fsm.action_window_equipment_tags
|
||||
msgid "Equipment Tag"
|
||||
msgstr "Étiquette d'équipement"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__summarized_equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__equipment_ids
|
||||
msgid "Equipment to Service"
|
||||
msgstr "Équipement à désservir"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
|
||||
msgid "Equipment:"
|
||||
msgstr "Équipement : "
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.actions.act_window,name:bemade_fsm.act_res_partner_2_equipment
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.partner_equipment_location_view_form
|
||||
msgid "Equipments"
|
||||
msgstr "Équipements"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__task_duration
|
||||
msgid "Estimated Duration"
|
||||
msgstr "Durée estimée"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,name:bemade_fsm.planning_project_stage_exception
|
||||
msgid "Exception"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__failed_message_ids
|
||||
msgid "Failed Messages"
|
||||
msgstr "Messages échoués"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.bemade_fsm_project_task_form_inherit
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.partner_equipment_location_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.sale_order_form_inherit
|
||||
msgid "Field Service"
|
||||
msgstr "Service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_bemade_fsm_equipment
|
||||
msgid "Field service equipment"
|
||||
msgstr "Équipement de service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_bemade_fsm_equipment_tag
|
||||
msgid "Field service equipment category"
|
||||
msgstr "Catégorie d'équipement de service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_bemade_fsm_equipment_type
|
||||
msgid "Field service equipment type"
|
||||
msgstr "Type d'équipement de service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_follower_ids
|
||||
msgid "Followers"
|
||||
msgstr "Abonnés"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_partner_ids
|
||||
msgid "Followers (Partners)"
|
||||
msgstr "Abonnés (partenaires)"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__activity_type_icon
|
||||
msgid "Font awesome icon e.g. fa-tasks"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_fully_delivered
|
||||
msgid "Fully Delivered"
|
||||
msgstr "Complètement livré"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_fully_delivered_and_invoiced
|
||||
msgid "Fully Invoiced"
|
||||
msgstr "Complètement facturé"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
|
||||
msgid "Group By"
|
||||
msgstr "Regrouper par"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__has_message
|
||||
msgid "Has Message"
|
||||
msgstr "A un message"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_exception_icon
|
||||
msgid "Icon"
|
||||
msgstr "Icône"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__activity_exception_icon
|
||||
msgid "Icon to indicate an exception activity."
|
||||
msgstr "Icône pour indiquer une activité d'exceiption."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_needaction
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_unread
|
||||
msgid "If checked, new messages require your attention."
|
||||
msgstr "Si coché, de nouveaux messages nécessitent votre attention."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_has_error
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_has_sms_error
|
||||
msgid "If checked, some messages have a delivery error."
|
||||
msgstr "Si coché, des messages ont une erreur de livraison."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,legend_normal:bemade_fsm.planning_project_stage_exception
|
||||
#: model:project.task.type,legend_normal:bemade_fsm.planning_project_stage_waiting_parts
|
||||
#: model:project.task.type,legend_normal:bemade_fsm.planning_project_stage_work_completed
|
||||
msgid "In Progress"
|
||||
msgstr "En cours"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_visit__is_invoiced
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_sale_order_line__is_fully_delivered_and_invoiced
|
||||
msgid ""
|
||||
"Indicates whether a line or all the lines in a section have be enentirely"
|
||||
" delivered and invoiced."
|
||||
msgstr ""
|
||||
"Indique si une ligne ou toutes les lignes dans une section ont été entièrement livrées"
|
||||
"et facturées."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_visit__is_completed
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_sale_order_line__is_fully_delivered
|
||||
msgid ""
|
||||
"Indicates whether a line or all the lines in a section have been entirely "
|
||||
"delivered."
|
||||
msgstr ""
|
||||
"Indique si une ligne ou toutes les lignes dans une section ont été entièrement livrées."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__planned_hours
|
||||
msgid "Initially Planned Hours"
|
||||
msgstr "Heures planifiées initialement"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__name
|
||||
msgid "Intervention Name"
|
||||
msgstr "Nom d'intervention"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__task_ids
|
||||
msgid "Interventions"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__is_invoiced
|
||||
msgid "Invoiced"
|
||||
msgstr "Facturé"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_field_service
|
||||
msgid "Is Field Service"
|
||||
msgstr "Est un service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_is_follower
|
||||
msgid "Is Follower"
|
||||
msgstr "Est un abonné"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__is_site_contact
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__is_site_contact
|
||||
msgid "Is a site contact"
|
||||
msgstr "Est un contact du site"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__label
|
||||
msgid "Label"
|
||||
msgstr "Nom"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment____last_update
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag____last_update
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type____last_update
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit____last_update
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__write_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__write_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__write_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__write_uid
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Modifié par"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__write_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__write_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__write_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__write_date
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Dernière mise à jour"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
|
||||
msgid "Location"
|
||||
msgstr "Emplacement"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_main_attachment_id
|
||||
msgid "Main Attachment"
|
||||
msgstr "Pièce jointe principale"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.bemade_fsm_project_task_form_inherit
|
||||
msgid "Mark as Delivered"
|
||||
msgstr "Marquer comme livré"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr "Erreur de livraison de message"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_ids
|
||||
msgid "Messages"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: code:addons/bemade_fsm/models/task.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Modifying the planned start or end time on a task is not permitted when that"
|
||||
" task already has planning records associated to it. Please modify or delete"
|
||||
" the planning records instead."
|
||||
msgstr ""
|
||||
"Modifier la date planifiée pour le début ou la fin d'une tâche nest pas permis quand"
|
||||
" cette tâche est déjà associée à des enregistrements de planification. Veuillez"
|
||||
" plutôt modifier ou supprimer les planifications associées."
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__my_activity_date_deadline
|
||||
msgid "My Activity Deadline"
|
||||
msgstr "Date d'échéance de mon activité"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__name
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__name
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_calendar_event_id
|
||||
msgid "Next Activity Calendar Event"
|
||||
msgstr "Évènement au calendrier de la prochaine activité"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_date_deadline
|
||||
msgid "Next Activity Deadline"
|
||||
msgstr "Échéance de la prochaine activité"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_summary
|
||||
msgid "Next Activity Summary"
|
||||
msgstr "Résumé de la prochaine activité"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_type_id
|
||||
msgid "Next Activity Type"
|
||||
msgstr "Type de la prochaine activité"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_needaction_counter
|
||||
msgid "Number of Actions"
|
||||
msgstr "Nombre d'actions"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_has_error_counter
|
||||
msgid "Number of errors"
|
||||
msgstr "Nombre d'erreurs"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_needaction_counter
|
||||
msgid "Number of messages which requires an action"
|
||||
msgstr "Nombre de messages nécessitant une action"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr "Nombre de messages avec une erreur de livraison"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__message_unread_counter
|
||||
msgid "Number of unread messages"
|
||||
msgstr "Nombre de messages non-lus"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "Ordered"
|
||||
msgstr "Commandé"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__owned_equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__owned_equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__valid_equipment_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__valid_equipment_ids
|
||||
msgid "Owned Equipments"
|
||||
msgstr "Équipements possédés"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__pid_tag
|
||||
msgid "P&ID Tag"
|
||||
msgstr "Tag P&ID"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
|
||||
msgid "Parent Task"
|
||||
msgstr "Tâche parent"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__parent
|
||||
msgid "Parent Task Template"
|
||||
msgstr "Gabarit de tâche parent"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__partner_location_id
|
||||
msgid "Physical Address"
|
||||
msgstr "Adresse physique"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__location_notes
|
||||
msgid "Physical Location Notes"
|
||||
msgstr "Notes sur l'emplacement physique"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_product_product__is_field_service
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_product_template__is_field_service
|
||||
msgid "Plan as field service"
|
||||
msgstr "Planifier comme service terrain"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Modèle de produit"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_product_product__is_field_service
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_product_template__is_field_service
|
||||
msgid ""
|
||||
"Products planned as field service will have travel time considered in "
|
||||
"planning."
|
||||
msgstr ""
|
||||
"Les produits planifiés comme service terrain auront leur temps de voyagement considéré"
|
||||
" lors de la planification."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
|
||||
msgid "Project"
|
||||
msgstr "Projet"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,legend_done:bemade_fsm.planning_project_stage_exception
|
||||
#: model:project.task.type,legend_done:bemade_fsm.planning_project_stage_waiting_parts
|
||||
#: model:project.task.type,legend_done:bemade_fsm.planning_project_stage_work_completed
|
||||
msgid "Ready"
|
||||
msgstr "Prêt"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__relevant_order_lines
|
||||
msgid "Relevant Order Lines"
|
||||
msgstr "Articles commandés pertinents"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_bemade_fsm_visit
|
||||
msgid "Represents a single visit by assigned service personnel."
|
||||
msgstr "Représente une seule visite par le personnel de service assigné."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_user_id
|
||||
msgid "Responsible User"
|
||||
msgstr "Utilisateur responsable"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_has_sms_error
|
||||
msgid "SMS Delivery error"
|
||||
msgstr "Erreur de livraison SMS"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__so_section_id
|
||||
msgid "Sale Order Section"
|
||||
msgstr "Section de commande de vente"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_sale_order
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__sale_order_id
|
||||
msgid "Sales Order"
|
||||
msgstr "Commande de vente"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_sale_order_line
|
||||
msgid "Sales Order Line"
|
||||
msgstr "Ligne de commandes de vente"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__sequence
|
||||
msgid "Sequence"
|
||||
msgstr "Séquence"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__task_id
|
||||
msgid "Service Visit"
|
||||
msgstr "Visite de service"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.sale_order_form_inherit
|
||||
msgid "Service Visits"
|
||||
msgstr "Visites de service"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_signature_block
|
||||
msgid "Signature"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__site_contacts
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__site_contacts
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__site_contacts
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__site_contacts
|
||||
msgid "Site Contacts"
|
||||
msgstr "Contacts sur site"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
|
||||
msgid "Site Contacts:"
|
||||
msgstr "Contacts sur site :"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__equipment_ids
|
||||
msgid "Site Equipment"
|
||||
msgstr "Équipements du site"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__planned_date_begin
|
||||
msgid "Start Date"
|
||||
msgstr "Date de début"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__activity_state
|
||||
msgid ""
|
||||
"Status based on activities\n"
|
||||
"Overdue: Due date is already passed\n"
|
||||
"Today: Activity date is today\n"
|
||||
"Planned: Future activities."
|
||||
msgstr ""
|
||||
"Status basé sur les activités\n"
|
||||
"En retard: La date est déjà passée\n"
|
||||
"Aujourd'hui: L'activité est dûe aujourd'hui\n"
|
||||
"Planifié: Activités futures."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__subtasks
|
||||
msgid "Subtask Templates"
|
||||
msgstr "Gabarits des sous-tâches"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
|
||||
msgid "Subtasks"
|
||||
msgstr "Sous-tâches"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
|
||||
msgid "Tag"
|
||||
msgstr "Étiquette"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.constraint,message:bemade_fsm.constraint_bemade_fsm_equipment_tag_name_uniq
|
||||
msgid "Tag name already exists !"
|
||||
msgstr "Le nom d'étiquette existe déjà !"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_project_task
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__task_ids
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
|
||||
msgid "Task"
|
||||
msgstr "Tâche"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.actions.act_window,name:bemade_fsm.task_template_act_window
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_product_product__task_template_id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_product_template__task_template_id
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_tree_view
|
||||
msgid "Task Template"
|
||||
msgstr "Gabarit de tâche"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.ui.menu,name:bemade_fsm.project_task_template_menu
|
||||
#: model:ir.ui.menu,name:bemade_fsm.service_task_template_meny
|
||||
msgid "Task Templates"
|
||||
msgstr "Gabarits de tâches"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__name
|
||||
msgid "Task Title"
|
||||
msgstr "Nom de tâche"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
|
||||
msgid "Technician"
|
||||
msgstr "Technicien·ne"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
|
||||
msgid "Technicians:"
|
||||
msgstr "Technicien·ne·s:"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model,name:bemade_fsm.model_project_task_template
|
||||
msgid "Template for new project tasks"
|
||||
msgstr "Gabarits pour nouvelles tâches"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_sale_order__default_equipment_ids
|
||||
msgid "The default equipment to service for new sale order lines."
|
||||
msgstr "L'équipement à désservir par défaut pour les nouveaux articles de commande de"
|
||||
" ventes."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_visit__so_section_id
|
||||
msgid ""
|
||||
"The section on the sale order that represents the labour and parts for this "
|
||||
"visit"
|
||||
msgstr ""
|
||||
"La section sur la commande de vente représentant la main d'oeuvre et les pièces pour"
|
||||
" cette visite"
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.actions.act_window,help:bemade_fsm.task_template_act_window
|
||||
msgid "There are no task templates, click above to create one."
|
||||
msgstr "Il n'existe aucun gabarit de tâche, cliquez ci-dessus pour en créer une."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: code:addons/bemade_fsm/models/sale_order.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-"
|
||||
"id=%d>%s</a> (%s)"
|
||||
msgstr ""
|
||||
"Cette tâche a été créée à partir de : <a href=# data-oe-model=sale.order data-oe-"
|
||||
"id=%d>%s</a> (%s)"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "Time & Material"
|
||||
msgstr "Temps & matériaux"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__activity_exception_decoration
|
||||
msgid "Type of the exception activity on record."
|
||||
msgstr "Type d'activité d'exception sur l'enregistrement."
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
|
||||
msgid "Unit Price"
|
||||
msgstr "Prix unitaire"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_unread
|
||||
msgid "Unread Messages"
|
||||
msgstr "Messages non-lus"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_unread_counter
|
||||
msgid "Unread Messages Counter"
|
||||
msgstr "Décompte de messages non-lus"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
|
||||
msgid "View Task"
|
||||
msgstr "Voir tâche"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__visit_id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__visit_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__visit_id
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__visit_ids
|
||||
msgid "Visit"
|
||||
msgstr "Visite"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,name:bemade_fsm.planning_project_stage_waiting_parts
|
||||
msgid "Waiting on Parts"
|
||||
msgstr "En attente de pièces"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__website_message_ids
|
||||
msgid "Website Messages"
|
||||
msgstr "Messages du site web"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__website_message_ids
|
||||
msgid "Website communication history"
|
||||
msgstr "Historique de communication du site web"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:project.task.type,name:bemade_fsm.planning_project_stage_work_completed
|
||||
msgid "Work Executed"
|
||||
msgstr "Travaux exécutés"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
|
||||
msgid "Work Order"
|
||||
msgstr "Bon de travail"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
|
||||
msgid ""
|
||||
"Work Order\n"
|
||||
" Contacts:"
|
||||
msgstr ""
|
||||
"Bon de travail\n"
|
||||
" Contacts :"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__work_order_contacts
|
||||
msgid "Work Order Contacts"
|
||||
msgstr "Contacts recevant le bon de travail"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__work_order_number
|
||||
msgid "Work Order Number"
|
||||
msgstr "Numéro de bon de travail"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__work_order_contacts
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__work_order_contacts
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__work_order_contacts
|
||||
msgid "Work Order Recipients"
|
||||
msgstr "Destinataires du bon de travail"
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__site_ids
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__site_ids
|
||||
msgid "Work Sites"
|
||||
msgstr "Emplacements pour service terrain"
|
||||
|
|
@ -4,3 +4,4 @@ from . import sale_order
|
|||
from . import equipment
|
||||
from . import task
|
||||
from . import res_partner
|
||||
from . import fsm_visit
|
||||
|
|
|
|||
|
|
@ -35,13 +35,8 @@ class Equipment(models.Model):
|
|||
|
||||
tag_ids = fields.Many2many('bemade_fsm.equipment.tag',
|
||||
string='Application',
|
||||
tracking=True,
|
||||
help="Classify and analyze your equipment categories like: Boiler, Laboratory, "
|
||||
"Waste water, Pure water")
|
||||
partner_id = fields.Many2one('res.partner',
|
||||
string="Owner",
|
||||
compute="_compute_partner",
|
||||
search="_search_partner",)
|
||||
|
||||
description = fields.Text(string="Description",
|
||||
tracking=True)
|
||||
|
|
@ -53,24 +48,17 @@ class Equipment(models.Model):
|
|||
|
||||
location_notes = fields.Text(string="Physical Location Notes",
|
||||
tracking=True)
|
||||
task_ids = fields.One2many(comodel_name='project.task',
|
||||
inverse_name='equipment_id',
|
||||
string='Interventions')
|
||||
task_ids = fields.Many2many(comodel_name='project.task',
|
||||
relation="bemade_fsm_task_equipment_rel",
|
||||
column1="equipment_id",
|
||||
column2="task_id",
|
||||
string='Interventions')
|
||||
|
||||
@api.depends('partner_location_id')
|
||||
def _compute_partner(self):
|
||||
for rec in self:
|
||||
rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor
|
||||
|
||||
@api.model
|
||||
def _search_partner(self, operator, value):
|
||||
return [('partner_location_id.root_ancestor', operator, value)]
|
||||
|
||||
@api.depends('pid_tag', 'name')
|
||||
def _compute_complete_name(self):
|
||||
for rec in self:
|
||||
rec.complete_name = "[%s] %s" % (rec.pid_tag or ' ', rec.name)
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
args = args or []
|
||||
|
|
@ -79,7 +67,6 @@ class Equipment(models.Model):
|
|||
'|', '|', '|',
|
||||
('pid_tag', operator, name),
|
||||
('name', operator, name),
|
||||
('partner_id.name', operator, name),
|
||||
('partner_location_id.name', operator, name)],
|
||||
limit=limit)
|
||||
else:
|
||||
|
|
@ -96,3 +83,9 @@ class Equipment(models.Model):
|
|||
'res_model': 'bemade_fsm.equipment',
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
|
||||
@api.depends('pid_tag', 'name')
|
||||
def _compute_complete_name(self):
|
||||
for rec in self:
|
||||
tag_part = "[%s] " % rec.pid_tag if rec.pid_tag else ""
|
||||
rec.complete_name = tag_part + rec.name
|
||||
|
|
|
|||
58
bemade_fsm/models/fsm_visit.py
Normal file
58
bemade_fsm/models/fsm_visit.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FSMVisit(models.Model):
|
||||
_name = "bemade_fsm.visit"
|
||||
_description = 'Represents a single visit by assigned service personnel.'
|
||||
|
||||
label = fields.Text(string="Label",
|
||||
required=True,
|
||||
related='so_section_id.name',
|
||||
readonly=False)
|
||||
approx_date = fields.Date(string='Approximate Date')
|
||||
so_section_id = fields.Many2one(comodel_name="sale.order.line",
|
||||
string="Sale Order Section",
|
||||
help="The section on the sale order that represents the labour and parts for "
|
||||
"this visit",
|
||||
ondelete="cascade")
|
||||
sale_order_id = fields.Many2one(comodel_name="sale.order",
|
||||
string="Sales Order",
|
||||
required=True)
|
||||
is_completed = fields.Boolean(string="Completed",
|
||||
related="so_section_id.is_fully_delivered")
|
||||
is_invoiced = fields.Boolean(string="Invoiced",
|
||||
related="so_section_id.is_fully_delivered_and_invoiced")
|
||||
summarized_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
compute="_compute_summarized_equipment_ids")
|
||||
task_id = fields.Many2one(comodel_name="project.task",
|
||||
compute="_compute_task_id",
|
||||
string="Service Visit",)
|
||||
task_ids = fields.One2many(comodel_name="project.task",
|
||||
inverse_name="visit_id")
|
||||
|
||||
@api.depends('task_ids')
|
||||
def _compute_task_id(self):
|
||||
for rec in self:
|
||||
rec.task_id = rec.task_ids and rec.task_ids[0]
|
||||
|
||||
@api.depends('so_section_id', 'sale_order_id.summary_equipment_ids')
|
||||
def _compute_summarized_equipment_ids(self):
|
||||
for rec in self:
|
||||
lines = rec.so_section_id.get_section_line_ids()
|
||||
equipment_ids = []
|
||||
for line in lines:
|
||||
for equipment in line.equipment_ids:
|
||||
equipment_ids.append(equipment)
|
||||
rec.summarized_equipment_ids = equipment_ids
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for i, rec in enumerate(recs):
|
||||
rec.so_section_id = rec.env['sale.order.line'].create({
|
||||
'order_id': rec.sale_order_id.id,
|
||||
'display_type': 'line_section',
|
||||
'name': vals_list[i]['label'],
|
||||
})
|
||||
return recs
|
||||
|
|
@ -5,3 +5,6 @@ class ProductTemplate(models.Model):
|
|||
_inherit = 'product.template'
|
||||
|
||||
task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict')
|
||||
is_field_service = fields.Boolean("Plan as field service",
|
||||
help="Products planned as field service will have travel time "
|
||||
"considered in planning.")
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from odoo import api, fields, models, Command
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
|
|
@ -7,17 +8,16 @@ class Partner(models.Model):
|
|||
string='Equipment Count')
|
||||
|
||||
owned_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
inverse_name="partner_id",
|
||||
compute="_compute_owned_equipment_ids",
|
||||
string="Owned Equipments")
|
||||
|
||||
|
||||
equipment_ids = fields.One2many(comodel_name='bemade_fsm.equipment',
|
||||
inverse_name='partner_location_id',
|
||||
string='Site Equipment')
|
||||
|
||||
is_site_contact = fields.Boolean(string='Is a site contact',
|
||||
compute="_compute_is_site_contact",
|
||||
search="_search_is_site_contact",)
|
||||
search="_search_is_site_contact", )
|
||||
|
||||
site_ids = fields.Many2many(string='Work Sites',
|
||||
comodel_name='res.partner',
|
||||
|
|
@ -42,6 +42,13 @@ class Partner(models.Model):
|
|||
domain=[('is_company', '=', False)],
|
||||
tracking=True)
|
||||
|
||||
@api.depends('equipment_ids', 'child_ids.company_type', 'child_ids.equipment_ids')
|
||||
def _compute_owned_equipment_ids(self):
|
||||
for rec in self:
|
||||
ids = rec.equipment_ids | rec.child_ids.filtered(
|
||||
lambda l: l.company_type == 'company').mapped('equipment_ids')
|
||||
rec.owned_equipment_ids = ids or False
|
||||
|
||||
@api.depends('site_ids')
|
||||
def _compute_is_site_contact(self):
|
||||
for rec in self:
|
||||
|
|
@ -50,7 +57,8 @@ class Partner(models.Model):
|
|||
@api.depends('equipment_ids')
|
||||
def _compute_equipment_count(self):
|
||||
for rec in self:
|
||||
all_equipment_ids = self.env['bemade_fsm.equipment'].search([('partner_id', '=', rec.id)])
|
||||
all_equipment_ids = self.env['bemade_fsm.equipment'].search(
|
||||
[('partner_location_id', '=', rec.id)])
|
||||
rec.equipment_count = len(all_equipment_ids)
|
||||
|
||||
@api.model
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_round
|
||||
import re
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
equipment_id = fields.Many2one(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
tracking=True)
|
||||
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
related="partner_id.owned_equipment_ids")
|
||||
default_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Default Equipment to Service",
|
||||
help="The default equipment to service for new sale order lines.",
|
||||
compute="_compute_default_equipment",
|
||||
inverse="_inverse_default_equipment",
|
||||
store=True, )
|
||||
|
||||
summary_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment Being Serviced",
|
||||
compute="_compute_summary_equipment_ids")
|
||||
|
||||
site_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
relation="sale_order_site_contacts_rel",
|
||||
|
|
@ -21,8 +33,32 @@ class SaleOrder(models.Model):
|
|||
inverse='_inverse_default_contacts',
|
||||
string='Work Order Recipients',
|
||||
store=True)
|
||||
visit_ids = fields.One2many(comodel_name='bemade_fsm.visit',
|
||||
inverse_name="sale_order_id",
|
||||
readonly=False)
|
||||
|
||||
@api.depends('partner_id')
|
||||
@api.depends('order_line.task_id')
|
||||
def get_relevant_order_lines(self, task_id):
|
||||
self.ensure_one()
|
||||
linked_lines = self.order_line.filtered(lambda l: l.task_id == task_id
|
||||
or l == task_id.visit_id.so_section_id)
|
||||
visit_lines = linked_lines.filtered(lambda l: l.visit_id)
|
||||
for line in visit_lines:
|
||||
linked_lines |= line.get_section_line_ids()
|
||||
return linked_lines
|
||||
|
||||
@api.depends('order_line.equipment_ids')
|
||||
def _compute_summary_equipment_ids(self):
|
||||
for rec in self:
|
||||
rec.summary_equipment_ids = rec.order_line.mapped('equipment_ids')
|
||||
|
||||
@api.onchange('partner_shipping_id')
|
||||
def _onchange_partner_shipping_id(self):
|
||||
super()._onchange_partner_shipping_id()
|
||||
self._compute_default_equipment()
|
||||
self._compute_default_contacts()
|
||||
|
||||
@api.depends('partner_shipping_id')
|
||||
def _compute_default_contacts(self):
|
||||
for rec in self:
|
||||
rec.site_contacts = rec.partner_shipping_id.site_contacts
|
||||
|
|
@ -31,18 +67,69 @@ class SaleOrder(models.Model):
|
|||
def _inverse_default_contacts(self):
|
||||
pass
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_equipment(self):
|
||||
@api.depends('partner_id', 'partner_shipping_id',
|
||||
'partner_shipping_id.equipment_ids',
|
||||
'partner_id.owned_equipment_ids')
|
||||
def _compute_default_equipment(self):
|
||||
for rec in self:
|
||||
rec.equipment_ids = self.partner_shipping_id.equipment_ids if len(
|
||||
self.partner_shipping_id.equipment_ids) <= 1 else False
|
||||
if rec.partner_shipping_id.equipment_ids:
|
||||
ids = rec.partner_shipping_id.equipment_ids
|
||||
else:
|
||||
ids = rec.partner_id.owned_equipment_ids
|
||||
rec.default_equipment_ids = ids if len(ids) < 4 else False
|
||||
|
||||
def _inverse_equipment(self):
|
||||
def _inverse_default_equipment(self):
|
||||
pass
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
related="order_id.valid_equipment_ids")
|
||||
visit_ids = fields.One2many(comodel_name="bemade_fsm.visit",
|
||||
inverse_name="so_section_id", )
|
||||
visit_id = fields.Many2one(comodel_name="bemade_fsm.visit",
|
||||
compute="_compute_visit_id",
|
||||
string="Visit",
|
||||
ondelete='cascade',
|
||||
store=True)
|
||||
is_fully_delivered = fields.Boolean(string="Fully Delivered",
|
||||
compute="_compute_is_fully_delivered",
|
||||
help="Indicates whether a line or all the lines in a section have been"
|
||||
" entirely delivered.")
|
||||
is_fully_delivered_and_invoiced = fields.Boolean(string="Fully Invoiced",
|
||||
compute="_compute_is_fully_invoiced",
|
||||
help="Indicates whether a line or all the lines in a section have been"
|
||||
" entirely delivered and invoiced.")
|
||||
equipment_ids = fields.Many2many(string="Equipment to Service",
|
||||
comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_equipment_sale_order_line_rel",
|
||||
column1="sale_order_line_id",
|
||||
column2="equipment_id")
|
||||
is_field_service = fields.Boolean(string="Is Field Service",
|
||||
compute="_compute_is_field_service",
|
||||
store=True)
|
||||
|
||||
task_duration = fields.Float(string="Estimated Duration",
|
||||
compute="_compute_task_duration", )
|
||||
|
||||
@api.depends('visit_ids')
|
||||
def _compute_visit_id(self):
|
||||
for rec in self:
|
||||
rec.visit_id = rec.visit_ids and rec.visit_ids[0]
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_is_field_service(self):
|
||||
for rec in self:
|
||||
rec.is_field_service = rec.product_id.is_field_service
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
recs = super().create(vals)
|
||||
for rec in recs:
|
||||
if rec.order_id.default_equipment_ids and not rec.equipment_ids:
|
||||
rec.equipment_ids = rec.order_id.default_equipment_ids
|
||||
return recs
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
""" Generate task for the given so line, and link it.
|
||||
|
|
@ -58,7 +145,9 @@ class SaleOrderLine(models.Model):
|
|||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
values = _timesheet_create_task_prepare_values_from_template(project, template, parent)
|
||||
values = _timesheet_create_task_prepare_values_from_template(project,
|
||||
template,
|
||||
parent)
|
||||
task = self.env['project.task'].sudo().create(values)
|
||||
subtasks = []
|
||||
for t in template.subtasks:
|
||||
|
|
@ -69,7 +158,8 @@ class SaleOrderLine(models.Model):
|
|||
task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
|
||||
return task
|
||||
|
||||
def _timesheet_create_task_prepare_values_from_template(project, template, parent):
|
||||
def _timesheet_create_task_prepare_values_from_template(project, template,
|
||||
parent):
|
||||
""" Copies the values from a project.task.template over to the set of values used to create a project.task.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
|
|
@ -79,12 +169,14 @@ class SaleOrderLine(models.Model):
|
|||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
vals = self._timesheet_create_task_prepare_values(project)
|
||||
vals['name'] = f"{vals['name']} ({template.name})" if not parent else template.name
|
||||
vals['name'] = template.name
|
||||
vals['description'] = template.description or vals['description']
|
||||
vals['parent_id'] = parent and parent.id
|
||||
vals['user_ids'] = template.assignees.ids
|
||||
vals['tag_ids'] = template.tags.ids
|
||||
vals['planned_hours'] = template.planned_hours
|
||||
if template.equipment_ids:
|
||||
vals['equipment_ids'] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
|
|
@ -98,5 +190,132 @@ class SaleOrderLine(models.Model):
|
|||
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (
|
||||
self.order_id.id, self.order_id.name, self.product_id.name)
|
||||
task.message_post(body=task_msg)
|
||||
task.equipment_id = self.order_id.equipment_id
|
||||
if not task.equipment_ids and self.equipment_ids:
|
||||
task.equipment_ids = self.equipment_ids.ids
|
||||
task.planned_hours = self.task_duration
|
||||
return task
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
super()._timesheet_service_generation()
|
||||
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||
for line in visit_lines:
|
||||
task_ids = line.get_section_line_ids().mapped('task_id')
|
||||
if not task_ids:
|
||||
continue
|
||||
if len(set([task.project_id for task in task_ids])) > 1:
|
||||
# Can't group up the tasks if they're part of different projects
|
||||
return
|
||||
project_id = task_ids[0].project_id
|
||||
line.visit_id.task_id = line._generate_task_for_visit_line(project_id)
|
||||
task_ids.write({'parent_id': line.visit_id.task_id.id})
|
||||
self.mapped('task_id').synchronize_name_fsm()
|
||||
|
||||
def _generate_task_for_visit_line(self, project):
|
||||
self.ensure_one()
|
||||
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Temp Name',
|
||||
'description': f"Parent task for {self.order_id.name}, visit {self.name}",
|
||||
'project_id': project.id,
|
||||
'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids,
|
||||
'sale_order_id': self.order_id.id,
|
||||
'partner_id': self.order_id.partner_shipping_id.id,
|
||||
'visit_id': self.visit_id.id,
|
||||
'date_deadline': self.visit_id.approx_date,
|
||||
'planned_hours': self.task_duration,
|
||||
'user_ids': False, # Force to empty or it uses the current user
|
||||
})
|
||||
return task
|
||||
|
||||
@api.depends('order_id.order_line', 'display_type', 'qty_to_deliver',
|
||||
'order_id.order_line.qty_to_deliver',
|
||||
'order_id.order_line.display_type')
|
||||
def _compute_is_fully_delivered(self):
|
||||
for rec in self:
|
||||
rec.is_fully_delivered = rec._iterate_items_compute_bool(
|
||||
lambda l: l.qty_to_deliver == 0)
|
||||
|
||||
@api.depends('is_fully_delivered')
|
||||
def _compute_is_fully_invoiced(self):
|
||||
for rec in self:
|
||||
if not rec.is_fully_delivered:
|
||||
rec.is_fully_delivered_and_invoiced = False
|
||||
return
|
||||
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
|
||||
lambda l: l.qty_to_invoice == 0)
|
||||
|
||||
def get_section_line_ids(self):
|
||||
self.ensure_one()
|
||||
assert self.display_type == 'line_section', "Cannot get section lines for a non-section."
|
||||
found = False
|
||||
lines = []
|
||||
for line in self.order_id.order_line.sorted(lambda l: l.sequence):
|
||||
if line == self:
|
||||
found = True
|
||||
continue
|
||||
if not found:
|
||||
continue
|
||||
if line.display_type == 'line_section': # Stop when we hit the next section
|
||||
break
|
||||
else:
|
||||
lines.append(line)
|
||||
return self.env['sale.order.line'].union(*lines)
|
||||
|
||||
def _iterate_items_compute_bool(self, single_line_func):
|
||||
if not self.display_type:
|
||||
return single_line_func(self)
|
||||
elif self.display_type == 'line_note':
|
||||
return True
|
||||
else:
|
||||
for line in self.order_id.order_line:
|
||||
found = False
|
||||
if line == self:
|
||||
found = True
|
||||
if not found:
|
||||
continue
|
||||
if found and line.display_type == 'line_section':
|
||||
return True
|
||||
val = single_line_func(self)
|
||||
if not val:
|
||||
return val
|
||||
return True
|
||||
|
||||
@api.depends('task_duration', 'product_id', 'product_id.planning_enabled', 'state')
|
||||
def _compute_planning_hours_to_plan(self):
|
||||
# Override the method from sale_planning to use time estimates from the task
|
||||
# template if appropriate. Also compute the value for visit lines.
|
||||
planning_lines = self.filtered_domain([
|
||||
('product_id.planning_enabled', '=', True),
|
||||
('state', 'not in', ['draft', 'sent'])
|
||||
])
|
||||
for line in planning_lines:
|
||||
line.planning_hours_to_plan = line.task_duration
|
||||
for line in self - planning_lines:
|
||||
line.planning_hours_to_plan = 0.0
|
||||
|
||||
@api.depends('product_id', 'visit_id')
|
||||
def _compute_task_duration(self):
|
||||
uom_hour = self.env.ref('uom.product_uom_hour')
|
||||
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
templated_lines = self.filtered(lambda l: l.product_id.task_template_id
|
||||
and l.product_id.task_template_id.
|
||||
planned_hours)
|
||||
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||
regular_lines = self - templated_lines - visit_lines
|
||||
for line in regular_lines:
|
||||
if line.product_uom == uom_hour or line.product_uom == uom_unit:
|
||||
line.task_duration = line.product_uom_qty
|
||||
else:
|
||||
line.task_duration = float_round(
|
||||
line.product_uom._compute_quantity(line.product_uom_qty, uom_hour,
|
||||
raise_if_failure=False),
|
||||
precision_digits=2)
|
||||
for line in templated_lines:
|
||||
line.task_duration = line.product_id.task_template_id.planned_hours
|
||||
if line.product_uom_category_id == self.env.ref(
|
||||
'uom.product_uom_unit').category_id:
|
||||
line.task_duration *= line.product_uom_qty
|
||||
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||
for line in visit_lines:
|
||||
line.task_duration = sum(line.get_section_line_ids().
|
||||
mapped('task_duration'))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
from odoo import fields, models, api, Command, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.osv import expression
|
||||
from collections import defaultdict, namedtuple
|
||||
import re
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
equipment_id = fields.Many2one("bemade_fsm.equipment", string="Equipment to Service", tracking=True)
|
||||
equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_task_equipment_rel",
|
||||
column1="task_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",
|
||||
tracking=True, )
|
||||
|
||||
work_order_contacts = fields.Many2many(comodel_name="res.partner",
|
||||
relation="task_work_order_contact_rel",
|
||||
|
|
@ -23,7 +31,107 @@ class Task(models.Model):
|
|||
inverse="_inverse_contacts",
|
||||
store=True)
|
||||
|
||||
@api.depends('sale_line_id.order_id.site_contacts', 'sale_line_id.order_id.work_order_contacts')
|
||||
planned_date_begin = fields.Datetime("Start Date", tracking=True,
|
||||
task_dependency_tracking=True,
|
||||
compute="_compute_planned_dates",
|
||||
inverse="_inverse_planned_dates",
|
||||
store=True)
|
||||
planned_date_end = fields.Datetime("End Date", tracking=True,
|
||||
task_dependency_tracking=True,
|
||||
compute="_compute_planned_dates",
|
||||
inverse="_inverse_planned_dates",
|
||||
store=True)
|
||||
|
||||
# Override related field to make it return false if this is an FSM subtask
|
||||
allow_billable = fields.Boolean(string="Can be billed",
|
||||
related=False,
|
||||
compute="_compute_allow_billable",
|
||||
store=True)
|
||||
|
||||
visit_id = fields.Many2one(comodel_name='bemade_fsm.visit')
|
||||
|
||||
relevant_order_lines = fields.Many2many(comodel_name='sale.order.line',
|
||||
store=False,
|
||||
compute='_compute_relevant_order_lines', )
|
||||
|
||||
work_order_number = fields.Char(readonly=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
res = super().create(vals)
|
||||
for rec in res:
|
||||
if rec.sale_order_id:
|
||||
seq = 1
|
||||
prev_seqs = self.sale_order_id.tasks_ids and \
|
||||
self.sale_order_id.tasks_ids.mapped('work_order_number')
|
||||
if prev_seqs:
|
||||
pattern = re.compile(r"\d+$")
|
||||
matches = map(lambda n: pattern.search(n), prev_seqs)
|
||||
seq += max(map(lambda n: int(n.group(1)) if n else 0), matches)
|
||||
rec.work_order_number = rec.sale_order_id.name.replace('SO', 'WO', 1) \
|
||||
+ f"-{seq}"
|
||||
return res
|
||||
|
||||
@api.depends('sale_order_id')
|
||||
def _compute_relevant_order_lines(self):
|
||||
for rec in self:
|
||||
rec.relevant_order_lines = (
|
||||
rec.sale_order_id and rec.sale_order_id.get_relevant_order_lines(
|
||||
rec) or False)
|
||||
|
||||
def _get_closed_stage_by_project(self):
|
||||
""" Gets the stage representing completed tasks for each project in
|
||||
self.project_id. Copied from industry_fsm/.../project.py:217-221
|
||||
for consistency.
|
||||
|
||||
:returns: Dict of project.project -> project.task.type"""
|
||||
return {
|
||||
project:
|
||||
project.type_ids.filtered(lambda stage: stage.is_closed)[:1]
|
||||
or project.type_ids[-1:]
|
||||
for project in self.project_id
|
||||
}
|
||||
|
||||
def _get_related_planning_slots(self):
|
||||
domain = expression.AND([
|
||||
self._get_domain_compute_forecast_hours(),
|
||||
[('task_id', 'in', self.ids + self._get_all_subtasks().ids)]
|
||||
])
|
||||
return self.env['planning.slot'].search(domain)
|
||||
|
||||
@api.depends('forecast_hours')
|
||||
def _compute_planned_dates(self):
|
||||
forecast_data = self._get_related_planning_slots()
|
||||
mapped_data = {}
|
||||
TimeSpan = namedtuple('timespan', ['start', 'end'])
|
||||
for d in forecast_data:
|
||||
if d not in mapped_data:
|
||||
mapped_data.update({d: TimeSpan(d.start_datetime, d.end_datetime)})
|
||||
continue
|
||||
if mapped_data[d].start > d.start_datetime:
|
||||
mapped_data[d].start = d.start_datetime
|
||||
if mapped_data[d].end < d.end_datetime:
|
||||
mapped_data[d].end = d.end_datetime
|
||||
for rec in self:
|
||||
if rec not in mapped_data:
|
||||
if not rec.planned_date_end:
|
||||
rec.planned_date_end = False
|
||||
if not rec.planned_date_begin:
|
||||
rec.planned_date_begin = False
|
||||
continue
|
||||
rec.planned_date_begin, rec.planned_date_end = mapped_data[rec]
|
||||
|
||||
def _inverse_planned_dates(self):
|
||||
""" Modifying the planned dates for tasks with existing planning records
|
||||
(planning.slot) has no defined safe behaviour, so we block it."""
|
||||
if self._get_related_planning_slots():
|
||||
raise UserError(_("Modifying the planned start or end time on a task is not "
|
||||
"permitted when that task already has planning records "
|
||||
"associated to it. Please modify or delete the planning "
|
||||
"records instead."))
|
||||
|
||||
@api.depends('sale_line_id.order_id.site_contacts',
|
||||
'sale_line_id.order_id.work_order_contacts')
|
||||
def _compute_contacts(self):
|
||||
""" The work order contacts and site contacts for a given task are taken from the sale order if the task
|
||||
is related to one, and from the task's customer if there is no sale order related to the task."""
|
||||
|
|
@ -45,3 +153,54 @@ class Task(models.Model):
|
|||
'work_order_contacts': [Command.set(rec.work_order_contacts.ids)],
|
||||
'site_contacts': [Command.set(rec.site_contacts.ids)],
|
||||
})
|
||||
|
||||
@api.depends('parent_id.visit_id', 'project_id.is_fsm', 'project_id.allow_billable')
|
||||
def _compute_allow_billable(self):
|
||||
for rec in self:
|
||||
# If an FSM task has a parent that is linked to an SO line, then the parent is the billable one
|
||||
if rec.parent_id and not rec.parent_id.visit_id and rec.project_id and rec.project_id.is_fsm:
|
||||
rec.allow_billable = False
|
||||
else:
|
||||
rec.allow_billable = rec.project_id.allow_billable
|
||||
|
||||
def action_fsm_validate(self):
|
||||
visits = self.filtered(lambda t: t.visit_id)
|
||||
non_visits = self - visits
|
||||
super(Task, non_visits).action_fsm_validate()
|
||||
|
||||
visits._stop_all_timers_and_create_timesheets()
|
||||
closed_stage_by_project = visits._get_closed_stage_by_project()
|
||||
super(Task, visits.child_ids).action_fsm_validate()
|
||||
for visit in visits:
|
||||
stage = closed_stage_by_project[visit.project_id]
|
||||
visits.write({'stage_id': stage.id, 'fsm_done': True})
|
||||
|
||||
def synchronize_name_fsm(self):
|
||||
""" Applies naming to the entire task tree for tasks that are part of this
|
||||
recordset. Root tasks are named:
|
||||
|
||||
Partner Shipping Name - Sale Line Name (Template Name)
|
||||
|
||||
Child tasks with sale_line_id are named by their template if set, sale line name
|
||||
if not.
|
||||
|
||||
Child tasks not linked to sale lines are left with their original names."""
|
||||
|
||||
all_tasks = self | self._get_all_subtasks()
|
||||
for rec in all_tasks:
|
||||
assert rec.is_fsm, "This method should only be called on FSM tasks."
|
||||
|
||||
template = rec.sale_line_id and rec.sale_line_id.product_id.task_template_id
|
||||
name_parts = rec.sale_line_id and rec.sale_line_id.name.split('\n')
|
||||
title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name
|
||||
if not rec.parent_id:
|
||||
rec.name = f"{rec.sale_order_id.partner_shipping_id.name} - " \
|
||||
f"{title}"
|
||||
if template:
|
||||
rec.name += f" ({template.name})"
|
||||
else:
|
||||
rec.name = template.name or title or rec.name
|
||||
|
||||
@property
|
||||
def root_ancestor(self):
|
||||
return self.parent_id and self.parent_id.root_ancestor or self
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo import models, fields, api, _, Command
|
||||
|
||||
|
||||
class TaskTemplate(models.Model):
|
||||
_name = 'project.task.template'
|
||||
_description = "Template for new project tasks"
|
||||
|
||||
@api.model
|
||||
def _current_company(self):
|
||||
return self.env.company
|
||||
|
||||
name = fields.Char(string="Task Title", required=True)
|
||||
description = fields.Html(string="Description")
|
||||
assignees = fields.Many2many("res.users", string="Default Assignees", help="Employees assigned to tasks created from this template.")
|
||||
customer = fields.Many2one("res.partner", string="Default Customer", help="Default customer for tasks created from this template.")
|
||||
project = fields.Many2one("project.project", string="Default Project", help="Default project for tasks created from this template.")
|
||||
tags = fields.Many2many("project.tags", string="Default Tags", help="Default tags for tasks created from this template.")
|
||||
assignees = fields.Many2many("res.users", string="Default Assignees",
|
||||
help="Employees assigned to tasks created from this template.")
|
||||
customer = fields.Many2one("res.partner", string="Default Customer",
|
||||
help="Default customer for tasks created from this template.")
|
||||
project = fields.Many2one("project.project", string="Default Project",
|
||||
help="Default project for tasks created from this template.")
|
||||
tags = fields.Many2many("project.tags", string="Default Tags",
|
||||
help="Default tags for tasks created from this template.")
|
||||
parent = fields.Many2one("project.task.template", string="Parent Task Template", ondelete='cascade')
|
||||
subtasks = fields.One2many("project.task.template", inverse_name="parent", string="Subtask Templates")
|
||||
sequence = fields.Integer(string="Sequence")
|
||||
company_id = fields.Many2one("res.company", string="Company", index=1, default=_current_company)
|
||||
planned_hours = fields.Float("Initially Planned Hours")
|
||||
equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_task_template_equipment_rel",
|
||||
column1="task_template_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",)
|
||||
|
||||
def action_open_task(self):
|
||||
return {
|
||||
|
|
@ -29,4 +39,8 @@ class TaskTemplate(models.Model):
|
|||
'context': self._context
|
||||
}
|
||||
|
||||
|
||||
@api.onchange('customer')
|
||||
def _onchange_customer(self):
|
||||
for rec in self:
|
||||
new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer]
|
||||
rec.write({'equipment_ids': [Command.set(new_equipment_ids)]})
|
||||
|
|
|
|||
371
bemade_fsm/reports/worksheet_custom_report_templates.xml
Normal file
371
bemade_fsm/reports/worksheet_custom_report_templates.xml
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="workorder_page_sale_order_table">
|
||||
<t t-set="order_lines" t-value="doc.relevant_order_lines"/>
|
||||
<t t-if="order_lines">
|
||||
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')"/>
|
||||
<t t-if="visit_lines" t-set="root_tasks"
|
||||
t-value="visit_lines.mapped('task_id')"/>
|
||||
<t t-else="" t-set="root_tasks"
|
||||
t-value="order_lines.mapped('task_id').filtered(lambda l: not l.parent_id)"/>
|
||||
<t t-set="final_subtotal" t-value="0"/>
|
||||
<t t-set="final_tax" t-value="0"/>
|
||||
<t t-set="final_total" t-value="0"/>
|
||||
<t t-set="is_any_total_discount_line" t-value="False"/>
|
||||
<t t-set="section_lines"
|
||||
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"/>
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="final_subtotal"
|
||||
t-value="final_subtotal + line.delivered_price_subtotal"/>
|
||||
<t t-set="final_total"
|
||||
t-value="final_total + line.delivered_price_total"/>
|
||||
<t t-set="final_tax"
|
||||
t-value="final_tax + line.delivered_price_tax"/>
|
||||
<t t-set="is_any_total_discount_line"
|
||||
t-value="is_any_total_discount_line or (line.discount and line.price_unit != 0 and line.delivered_price_total == 0)"/>
|
||||
</t>
|
||||
<t t-set="display_discount"
|
||||
t-value="any(line.discount for line in order_lines)"/>
|
||||
</t>
|
||||
<t t-else="" t-set="root_tasks"
|
||||
t-value="doc.root_ancestor"/>
|
||||
<div t-if="order_lines.filtered(lambda l: not l.display_type)">
|
||||
<h2 t-if="order_lines">Time & Material</h2>
|
||||
<div t-if="order_lines" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Description</th>
|
||||
<th class="text-right">Ordered</th>
|
||||
<th class="text-right">Delivered</th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
<th t-if="display_discount" class="text-right"
|
||||
groups="product.group_discount_per_so_line">
|
||||
<span>Disc.%</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span groups="account.group_show_line_subtotals_tax_excluded">
|
||||
Amount</span>
|
||||
<span groups="account.group_show_line_subtotals_tax_included">
|
||||
Total Price</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="is_task" t-value="line == doc.sale_line_id"/>
|
||||
<t t-set="is_total_discount"
|
||||
t-value="line.discount and line.price_unit != 0 and line.delivered_price_total == 0"/>
|
||||
<t t-set="current_subtotal"
|
||||
t-value="current_subtotal + line.delivered_price_subtotal"
|
||||
groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<t t-set="current_total"
|
||||
t-value="current_subtotal + line.delivered_price_total"
|
||||
groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''">
|
||||
<t t-if="not line.display_type and not line.is_downpayment">
|
||||
<td><span t-field="line.name"/></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
<span t-field="line.product_uom"
|
||||
groups="uom.group_uom"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.qty_delivered"/>
|
||||
<span t-field="line.product_uom"
|
||||
groups="uom.group_uom"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.price_unit"/>
|
||||
</td>
|
||||
<td t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="product.group_discount_per_so_line">
|
||||
<span t-field="line.discount"/>
|
||||
</td>
|
||||
<td class="text-right o_price_total">
|
||||
<span t-field="line.delivered_price_subtotal"/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
<t t-set="current_section" t-value="line"/>
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
<t t-if="current_section and len(section_lines) > 1 and (line_last or order_lines[line_index+1].display_type == 'line_section')">
|
||||
<tr class="is-subtotal text-right">
|
||||
<td colspan="99">
|
||||
<strong class="mr16">Section
|
||||
Subtotal</strong>
|
||||
<span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="container_subtotal" name="so_total_summary">
|
||||
<div id="total" class="row justify-content-end" name="total">
|
||||
<div t-attf-class="#{'col-auto' if report_type != 'html' else 'col-sm-2'}">
|
||||
<table class="table table-sm">
|
||||
<tr t-if="final_tax" class="border-black o_subtotal"
|
||||
style="">
|
||||
<td><strong>Untaxed amount</strong></td>
|
||||
<td class="text-right">
|
||||
<span t-esc="final_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_tax" class="border-black o_subtotal">
|
||||
<td><strong>Taxes</strong></td>
|
||||
<td class="text-right">
|
||||
<span t-esc="final_tax"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_total" class="border-black o_total">
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-right">
|
||||
<span t-esc="final_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="subtask_list">
|
||||
<ul t-if="task.child_ids" class="ml-1 pl-1" style="list-style-type: none;">
|
||||
<t t-foreach="task.child_ids" t-as="subtask">
|
||||
<li class="ml-1">
|
||||
<span t-out="subtask.name"/>
|
||||
<t t-call="bemade_fsm.subtask_list">
|
||||
<t t-set="task" t-value="subtask"/>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="workorder_page_tasks_table">
|
||||
<t t-set="interventions" t-value="doc.child_ids"/>
|
||||
<t t-set="timesheets" t-value="doc.timesheet_ids"/>
|
||||
<t t-foreach="interventions" t-as="intervention">
|
||||
<div style="page-break-inside: avoid;">
|
||||
<h2 t-out="str(intervention_index + 1) + '. ' + intervention.name"/>
|
||||
<h3 t-if="intervention.equipment_ids">Equipment:
|
||||
<t t-foreach="intervention.equipment_ids" t-as="equipment_id">
|
||||
<span t-out="equipment_id.complete_name + (', ' if not equipment_id_last else '')"/>
|
||||
</t>
|
||||
</h3>
|
||||
<div>
|
||||
<span t-esc="intervention.description"/>
|
||||
</div>
|
||||
<t t-set="tasks" t-value="intervention.child_ids"/>
|
||||
<div t-if="tasks" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="7%">Status</th>
|
||||
<th width="30%">Task</th>
|
||||
<th width="18%">Technician</th>
|
||||
<th width="45%">Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="tasks" t-as="task">
|
||||
<tr class="p-1 m-1">
|
||||
<td t-out="task.stage_id.name"/>
|
||||
<td>
|
||||
<span t-out="task.name"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-foreach="task.user_ids" t-as="user">
|
||||
<span t-out="user.name + (', ' if not user_last else '')"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-out="task.description or ''"/>
|
||||
</tr>
|
||||
<tr class="mt-0 pt-0">
|
||||
<td/>
|
||||
<td>
|
||||
<t t-call="bemade_fsm.subtask_list"/>
|
||||
</td>
|
||||
<td/>
|
||||
<td/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
<template id="workorder_page_info_block">
|
||||
<h1>Work Order <span t-out="doc.work_order_number"></span></h1>
|
||||
<div class="row">
|
||||
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}">
|
||||
<div t-if="doc.user_ids"><h6>Technicians: </h6></div>
|
||||
<t t-foreach="doc.user_ids" t-as="user">
|
||||
<div class="mb-3">
|
||||
<div t-esc="user" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "email"]
|
||||
}'/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}">
|
||||
<t t-if="doc.partner_id">
|
||||
<div><h6>Customer: </h6></div>
|
||||
<div t-esc="doc.partner_id" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address",]
|
||||
}'/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}">
|
||||
<div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div>
|
||||
<t t-foreach="doc.site_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div t-esc="contact" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}">
|
||||
<div t-if="doc.work_order_contacts"><h6>Work Order
|
||||
Contacts: </h6></div>
|
||||
<t t-foreach="doc.work_order_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div t-esc="contact" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_equipment_summary">
|
||||
<div t-if="doc.equipment_ids and len(doc.child_ids) > 1"
|
||||
style="page-break-inside: avoid;">
|
||||
<h3>Equipment Serviced</h3>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Name</th>
|
||||
<th>Application</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.equipment_ids" t-as="equipment">
|
||||
<tr>
|
||||
<td><span t-esc="equipment.pid_tag"/></td>
|
||||
<td><span t-esc="equipment.name"/></td>
|
||||
<td>
|
||||
<t t-foreach="equipment.tag_ids"
|
||||
t-as="application">
|
||||
<span t-esc="application.name + (', ' if not application_last else '')"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-esc="equipment.partner_location_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address"]}'
|
||||
/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_signature_block">
|
||||
<div t-if="doc.worksheet_signature">
|
||||
<div t-if="report_type == html" class="ribbon" style="
|
||||
position: absolute;
|
||||
right: 0px; top: 0px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
width: 75px; height: 75px;
|
||||
text-align: right;">
|
||||
<span style="
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-weight: bold; line-height: 20px;
|
||||
transform: rotate(45deg);
|
||||
width: 100px; height: auto; display: block;
|
||||
background: green;
|
||||
position: absolute;
|
||||
top: 19px; right: -21px; left: auto;
|
||||
padding: 0;">
|
||||
Signed
|
||||
</span>
|
||||
</div>
|
||||
<div t-attf-class="#{' col-12 col-lg-3' if report_type != 'html' else '
|
||||
col-sm-7 col-md-4'} ml-auto text-right"
|
||||
style="page-break-inside: avoid">
|
||||
<h5>Signature</h5>
|
||||
<img t-att-src="image_data_uri(doc.worksheet_signature)"
|
||||
style="max-height: 6rem; max-width: 100%; color:black;"/><br/>
|
||||
<span t-field="doc.worksheet_signed_by"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="work_order_page">
|
||||
<div class="page">
|
||||
<t t-call="bemade_fsm.workorder_page_info_block"/>
|
||||
<t t-call="bemade_fsm.workorder_page_sale_order_table"/>
|
||||
<t t-call="bemade_fsm.workorder_equipment_summary"/>
|
||||
<t t-call="bemade_fsm.workorder_page_signature_block"/>
|
||||
<t t-call="bemade_fsm.workorder_page_tasks_table"/>
|
||||
</div>
|
||||
</template>
|
||||
<template id="work_order">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-set="doc" t-value="doc.root_ancestor" t-if="doc.parent_id"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t t-call="bemade_fsm.work_order_page"
|
||||
t-lang="doc.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<!-- Make the front-end "Sign" page show our new template -->
|
||||
<template id="portal_my_worksheet"
|
||||
inherit_id="industry_fsm_report.portal_my_worksheet">
|
||||
<xpath expr="//div[@t-call='industry_fsm_report.worksheet_custom_page']"
|
||||
position="replace">
|
||||
<div t-call="bemade_fsm.work_order_page"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
13
bemade_fsm/reports/worksheet_custom_reports.xml
Normal file
13
bemade_fsm/reports/worksheet_custom_reports.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="industry_fsm_report.task_custom_report" model="ir.actions.report">
|
||||
<field name="name">Work Order Report (PDF)</field>
|
||||
<field name="report_name">bemade_fsm.work_order</field>
|
||||
<field name="report_file">bemade_fsm.work_order</field>
|
||||
<field name="print_report_name">'%s Work Order %s' % (
|
||||
time.strftime('%Y-%m-%d'), object.work_order_number)</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
146
bemade_fsm/reports/worksheet_templates.xml
Normal file
146
bemade_fsm/reports/worksheet_templates.xml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="default_worksheet_template" model="ir.actions.report">
|
||||
<field name="name">Complete Worksheet Report (PDF)</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">bemade_fsm.worksheet_complete</field>
|
||||
<field name="print_report_name">
|
||||
'%s Worksheet %s' % (time.strftime('%Y-%m-%d'), object.name)
|
||||
</field>
|
||||
<field name="binding_model_id" ref="model_project_task"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<template id="worksheet_complete">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="bemade_fsm.worksheet_complete_page"
|
||||
t-lang="doc.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="worksheet_complete_page">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="address" t-if="doc.sale_order_id">
|
||||
<strong class="big-red">Customer:</strong>
|
||||
<div t-field="doc.sale_order_id.partner_id"
|
||||
t-options='{"widget": "contact",
|
||||
"fields": ["address", "name"], "no_marker": True}'/>
|
||||
</t>
|
||||
<t t-set="information_block">
|
||||
<strong>Service Address:</strong>
|
||||
<div t-field="doc.partner_id"
|
||||
t-options='{"widget": "contact",
|
||||
"fields": ["address", "name", "phone"],
|
||||
"no_marker": True, "phone_icons": True}'/>
|
||||
</t>
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<h2 class="mt-3">
|
||||
<span>Work Order </span>
|
||||
<span t-if="doc.sale_order_id" t-field="doc.sale_order_id.name"/>
|
||||
<span t-else="" t-field="doc.name"/>
|
||||
</h2>
|
||||
<div class="container">
|
||||
<div class="row mb-3 border-dark">
|
||||
<div t-if="doc.sale_order_id.client_order_ref" class="col-4">
|
||||
<span class="font-weight-bolder">Your Reference:</span>
|
||||
<span t-field="doc.sale_order_id.client_order_ref"/>
|
||||
</div>
|
||||
<div t-if="doc.site_contacts" class="col-4">
|
||||
<span class="font-weight-bolder">Site Contacts:</span>
|
||||
<t t-foreach="doc.site_contacts" t-as="contact">
|
||||
<span t-field="contact.name"/>
|
||||
<span t-out="contact"
|
||||
t-options='{"widget": "contact",
|
||||
"fields": ["phone", "mobile", "email"],
|
||||
"no_marker": True, "phone_icons": True}'/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="doc.work_order_contacts" class="col-4">
|
||||
<span class="font-weight-bolder">Work order attn:</span>
|
||||
<t t-foreach="doc.work_order_contacts" t-as="contact">
|
||||
<span t-field="contact.name"/>
|
||||
<span t-out="contact"
|
||||
t-options='{"widget": "contact",
|
||||
"fields": ["phone", "mobile", "email"],
|
||||
"no_marker": True, "phone_icons": True}'/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3 border-dark">
|
||||
<div class="col-4" t-if="doc.user_ids">
|
||||
<span class="font-weight-bolder">Technician(s)</span>
|
||||
<t t-foreach="doc.user_ids" t-as="technician">
|
||||
<p class="m-0" t-field="technician.name"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-4" t-if="doc.planned_date_begin">
|
||||
<span class="font-weight-bolder">Planned Start</span>
|
||||
<p t-field="doc.planned_date_begin"
|
||||
t-options="{'widget': 'datetime'}"/>
|
||||
</div>
|
||||
<div class="col-4" t-if="doc.planned_date_end">
|
||||
<span class="font-weight-bolder">Planned End</span>
|
||||
<p t-field="doc.planned_date_end"
|
||||
t-options="{'widget': 'datetime'}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3 class="mb32"><strong>Interventions</strong></h3>
|
||||
<t t-foreach="doc.child_ids" t-as="intervention">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h4><span t-field="intervention.name"/></h4>
|
||||
<span t-if="intervention.description"
|
||||
t-field="intervention.description"/>
|
||||
</div>
|
||||
<div t-if="intervention.equipment_ids"
|
||||
class="col-6">
|
||||
<h4>Equipment: </h4>
|
||||
<t t-foreach="intervention.equipment_ids"
|
||||
t-as="equipment">
|
||||
<span t-field="equipment.complete_name"/>
|
||||
<span t-if="equipment_index < equipment_size -1"
|
||||
t-out="', '"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-borderless col-12">
|
||||
<thead>
|
||||
<th>Done</th>
|
||||
<th>Task to do</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="intervention.child_ids"
|
||||
t-as="task">
|
||||
<tr class="wo-task">
|
||||
<td style="width: 5%">
|
||||
<input type="checkbox"
|
||||
t-att-checked="task.is_complete"/>
|
||||
</td>
|
||||
<td style="width: 95%">
|
||||
<p t-field="task.name"/>
|
||||
<p t-field="task.description"/>
|
||||
</td>
|
||||
<!-- TODO: Figure out what to do with the old
|
||||
concept of task comments -->
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_task_template_manager,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template_user,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_equipment_tag,bemade_fsm_equipment_tag,model_bemade_fsm_equipment_tag,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
|
||||
access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
|
||||
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
14
bemade_fsm/static/src/scss/bemade_fsm.scss
Normal file
14
bemade_fsm/static/src/scss/bemade_fsm.scss
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This prevents repeating the header, which overlaps with the body due to a bug in
|
||||
wkhtmltopdf rendering.
|
||||
*/
|
||||
thead {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
const TEST_COMPANY = "Test Partner Company";
|
||||
const TEST_COMPANY = "Test Partner";
|
||||
const TEST_EQPT1 = "Test Equipment 1";
|
||||
const TEST_EQPT2 = "Test Equipment 2";
|
||||
tour.register('equipment_base_tour', {
|
||||
|
|
@ -21,7 +21,6 @@ tour.register('equipment_base_tour', {
|
|||
}, {
|
||||
content: 'Click the create button',
|
||||
trigger: '.o_list_button_add',
|
||||
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Equipment))',
|
||||
}, {
|
||||
content: 'Add a tag',
|
||||
trigger: 'input[name="pid_tag"]',
|
||||
|
|
@ -33,13 +32,13 @@ tour.register('equipment_base_tour', {
|
|||
}, {
|
||||
content: 'Set the partner',
|
||||
trigger: 'div[name="partner_location_id"] div div input',
|
||||
run: 'text Test Partner Company',
|
||||
run: `text ${TEST_COMPANY}`,
|
||||
}, {
|
||||
content: 'Click the partner in the dropdown',
|
||||
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Save equipment',
|
||||
trigger: 'button.o_form_button_save',
|
||||
trigger: 'button.o_list_button_save',
|
||||
}, {
|
||||
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
|
||||
content: 'Navigate to the Clients submenu',
|
||||
|
|
@ -65,7 +64,6 @@ tour.register('equipment_base_tour', {
|
|||
extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Make sure we have a first test equipment',
|
||||
/*trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT1}))`,*/
|
||||
trigger: `td:contains(${TEST_EQPT1})`,
|
||||
run: function () {
|
||||
},
|
||||
|
|
@ -100,13 +98,7 @@ tour.register('equipment_sale_order_tour', {
|
|||
content: 'Navigate to the Field Service tab.',
|
||||
trigger: 'a.nav-link[role="tab"]:contains(Field Service)'
|
||||
}, {
|
||||
content: 'Click Edit',
|
||||
trigger: 'button.o_form_button_edit',
|
||||
}, {
|
||||
content: 'Click the equipment dropdown.',
|
||||
trigger: '.o_field_widget[name="equipment_id"] input',
|
||||
}, {
|
||||
content: 'Check that the equipment is in the dropdown.',
|
||||
content: 'Check that the equipment is listed in the tab.',
|
||||
trigger: `li.ui-menu-item > a:contains(${TEST_EQPT1})`,
|
||||
}
|
||||
]);
|
||||
40
bemade_fsm/static/tests/tours/sale_order_tour.js
Normal file
40
bemade_fsm/static/tests/tours/sale_order_tour.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
const SO_NAME = "TEST ORDER 2"
|
||||
const PRODUCT_NAME = "Test Product 3"
|
||||
tour.register('sale_order_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
},
|
||||
[tour.stepUtils.showAppsMenuItem(), {
|
||||
content: 'Navigate to the Service menu',
|
||||
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
|
||||
}, {
|
||||
content: 'Search for the sales order',
|
||||
trigger: 'input.o_searchview_input',
|
||||
run: `text ${SO_NAME}`,
|
||||
}, {
|
||||
content: 'Validate Search',
|
||||
trigger: '.o_menu_item.o_selection_focus',
|
||||
run: 'click',
|
||||
}, {
|
||||
content: 'Open the test order',
|
||||
trigger: `.o_data_cell[name="client_order_ref"]:contains(${SO_NAME})`,
|
||||
}, {
|
||||
content: 'Click the view tasks button',
|
||||
trigger: 'button[name="action_view_task"]',
|
||||
}, /*{
|
||||
content: 'Click the first task',
|
||||
trigger: `div.o_kanban_record:has(span:contains(${PRODUCT_NAME}))`,
|
||||
},*/ {
|
||||
content: 'Click on the ready to invoice button',
|
||||
trigger: 'button[name="action_fsm_validate"]',
|
||||
extra_trigger: `li.breadcrumb-item.active:has(span:contains(${PRODUCT_NAME}))`
|
||||
}, {
|
||||
content: 'View the SO',
|
||||
trigger: 'button[name="action_view_so"]',
|
||||
// extra_trigger: 'button[title="Current state"]:contains(Done)',
|
||||
}
|
||||
]);
|
||||
|
|
@ -38,7 +38,7 @@ tour.register('task_equipment_tour', {
|
|||
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Save equipment',
|
||||
trigger: 'button.o_form_button_save',
|
||||
trigger: 'button.o_list_button_save',
|
||||
}, {
|
||||
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
|
||||
content: 'Navigate to the Clients submenu',
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from . import test_task_template
|
|||
from . import test_sale_order
|
||||
from . import test_equipment
|
||||
from . import test_fsm_contact_setting
|
||||
from . import test_so_task_contacts
|
||||
from . import test_fsm_visit
|
||||
|
|
|
|||
|
|
@ -3,37 +3,192 @@ from odoo import Command
|
|||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class FSMManagerUserTransactionCase(TransactionCase):
|
||||
class BemadeFSMBaseTest(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def _generate_project_manager_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
user_group_project_manager = cls.env.ref('project.group_project_manager')
|
||||
user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_manager')
|
||||
group_ids.append(user_group_fsm_manager.id)
|
||||
group_ids.append(user_group_project_manager.id)
|
||||
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def _generate_project_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def __generate_user(cls, name, login, group_ids):
|
||||
return cls.env['res.users'].with_context({'no_reset_password': True}).create({
|
||||
'name': name,
|
||||
'login': login,
|
||||
'password': login,
|
||||
'email': f"{login}@test.co",
|
||||
'groups_id': [Command.set(group_ids)]
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __get_user_groups(cls):
|
||||
user_group_employee = cls.env.ref('base.group_user')
|
||||
user_group_project_user = cls.env.ref('project.group_project_user')
|
||||
user_group_project_manager = cls.env.ref('project.group_project_manager')
|
||||
user_group_fsm_user = cls.env.ref('industry_fsm.group_fsm_user')
|
||||
user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_manager')
|
||||
user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager')
|
||||
user_group_sales_user = cls.env.ref('sales_team.group_sale_salesman')
|
||||
user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager')
|
||||
user_product_customer = cls.env.ref('customer_product_code.group_product_customer_code_user')
|
||||
|
||||
group_ids = [user_group_employee.id,
|
||||
user_group_project_user.id,
|
||||
user_group_project_manager.id,
|
||||
user_group_fsm_user.id,
|
||||
user_group_fsm_manager.id,
|
||||
user_group_sales_user.id,
|
||||
user_group_sales_manager.id, ]
|
||||
user_group_sales_manager.id,
|
||||
user_group_sales_user.id, ]
|
||||
if user_product_customer:
|
||||
group_ids.append(user_product_customer.id)
|
||||
return group_ids
|
||||
|
||||
# Test user with project access rights for the various tests
|
||||
Users = cls.env['res.users'].with_context({'no_reset_password': True})
|
||||
cls.user = Users.create({
|
||||
'name': 'Project Manager',
|
||||
'login': 'misterpm',
|
||||
'password': 'misterpm',
|
||||
'email': 'mrpm@testco.com',
|
||||
'signature': 'Mr. PM',
|
||||
'groups_id': [Command.set(group_ids)],
|
||||
@classmethod
|
||||
def _generate_partner(cls, name: str = 'Test Company', company_type: str = 'company', parent=None,
|
||||
location_type='other'):
|
||||
""" Generates a partner with basic address filled in.
|
||||
|
||||
:param name: The partner's name.
|
||||
:param company_type: The type of partner, either 'company' or 'person' are accepted."""
|
||||
return cls.env['res.partner'].create({
|
||||
'name': name,
|
||||
'company_type': company_type,
|
||||
'street': '123 Street St.',
|
||||
'city': 'Montreal',
|
||||
'state_id': cls.env.ref('base.state_ca_qc').id,
|
||||
'country_id': cls.env.ref('base.ca').id,
|
||||
'parent_id': parent and parent.id or False,
|
||||
'type': location_type,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None, shipping_location=None):
|
||||
partner = partner or cls._generate_partner()
|
||||
vals = {'partner_id': partner.id,
|
||||
'client_order_ref': client_order_ref}
|
||||
if equipment:
|
||||
vals.update({'default_equipment_ids': [Command.set([equipment.id])]})
|
||||
if shipping_location:
|
||||
vals.update({'partner_shipping_id': shipping_location.id})
|
||||
return cls.env['sale.order'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order_line(cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False):
|
||||
if not product:
|
||||
product = cls._generate_product()
|
||||
return cls.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': qty,
|
||||
'product_uom': uom and uom.id or cls.env.ref("uom.product_uom_hour").id,
|
||||
'price_unit': price,
|
||||
'tax_id': tax_id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_equipment(cls, name='test equipment', partner=None):
|
||||
return cls.env['bemade_fsm.equipment'].create({
|
||||
'name': name,
|
||||
'partner_location_id': partner and partner.id or False,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project',
|
||||
project=None, task_template=None, service_policy='delivered_manual', uom=None):
|
||||
if 'project' in service_tracking and not project:
|
||||
project = cls.env.ref("industry_fsm.fsm_project")
|
||||
uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False
|
||||
return cls.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': product_type,
|
||||
'service_tracking': service_tracking,
|
||||
'project_id': service_tracking in ('task_global_project', 'project_only') and project.id or False,
|
||||
'project_template_id': service_tracking == 'task_in_project' and project.id or False,
|
||||
'task_template_id': task_template and task_template.id or False,
|
||||
'service_policy': service_policy,
|
||||
'uom_id': uom_id,
|
||||
'uom_po_id': uom_id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_fsm_project(cls, name='Test Project'):
|
||||
return cls.env['project.project'].create({
|
||||
'name': name,
|
||||
'allow_material': True,
|
||||
'allow_timesheets': True,
|
||||
'allow_subtasks': True,
|
||||
'allow_quotations': True,
|
||||
'allow_worksheets': True,
|
||||
'is_fsm': True,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_task_template(cls, parent=None, structure=None, names=None, planned_hours=1,
|
||||
equipment=None, customer=None):
|
||||
""" Generates a task template with the specified structure and naming.
|
||||
|
||||
:param parent: The parent task template for the top-level task template being generated
|
||||
:param structure: A list of integers describing the number of tasks for each level of descendants. An empty
|
||||
list represents only one top-level task template. If no structure is given, an empty list
|
||||
will be used in its place.
|
||||
:param names: The name prefixes to be given to the task templates at each level. Each prefix will be followed
|
||||
by a sequential integer for its level. Child 1, Child 2, Grandchild 1, etc. If no names argument
|
||||
is passed, a default ['Task Template'] argument will be used.
|
||||
:param planned_hours: The number of planned hours for the top-level task template being generated.
|
||||
:param equipment: The equipment to add as linked equipment to the task template."""
|
||||
if not names:
|
||||
names = ['Task Template']
|
||||
if not structure:
|
||||
structure = []
|
||||
if len(structure) != len(names) - 1:
|
||||
raise ValueError("The length of the structure argument must contain exactly one element less than the "
|
||||
"names argument.")
|
||||
name = names.pop(0)
|
||||
template = cls.env['project.task.template'].create({
|
||||
'name': name,
|
||||
'parent': parent and parent.id or False,
|
||||
'planned_hours': planned_hours,
|
||||
'equipment_ids': [Command.set(equipment and [equipment.id] or [])],
|
||||
'customer': customer and customer.id or False,
|
||||
})
|
||||
parent = template
|
||||
while structure:
|
||||
subtasks = []
|
||||
for i in range(0, structure[0]):
|
||||
subtasks.append(cls.env['project.task.template'].create({
|
||||
'parent': parent.id,
|
||||
'name': names[0] + f" {i}",
|
||||
}))
|
||||
structure.pop(0)
|
||||
names.pop(0)
|
||||
parent = subtasks[0]
|
||||
return template
|
||||
|
||||
def _invoice_sale_order(self, so):
|
||||
wiz = self.env['sale.advance.payment.inv'].with_context(
|
||||
{'active_ids': [so.id]}).create({})
|
||||
wiz.create_invoices()
|
||||
inv = so.invoice_ids[-1]
|
||||
inv.action_post()
|
||||
return inv
|
||||
|
||||
def _generate_visit(self, sale_order, label="Test Label"):
|
||||
return self.env['bemade_fsm.visit'].create([{
|
||||
'sale_order_id': sale_order.id,
|
||||
'label': label,
|
||||
}])
|
||||
|
||||
def _generate_so_with_one_visit_two_lines(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
return so, visit, sol1, sol2
|
||||
|
|
|
|||
|
|
@ -1,57 +1,21 @@
|
|||
from odoo.tests.common import HttpCase, tagged
|
||||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo import Command
|
||||
from odoo.exceptions import MissingError
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestEquipmentCommon(FSMManagerUserTransactionCase):
|
||||
class TestEquipment(BemadeFSMBaseTest):
|
||||
def test_crud(self):
|
||||
partner_company = self._generate_partner()
|
||||
partner_contact = self._generate_partner('Site Contact', 'person', partner_company)
|
||||
equipment = self._generate_equipment('Test Equipment 1', partner_company)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Set up the test partner
|
||||
cls.partner_company = cls.env['res.partner'].create({
|
||||
'name': 'Test Partner Company',
|
||||
'company_type': 'company',
|
||||
'street': '123 Street St.',
|
||||
'city': 'Montreal',
|
||||
'state_id': cls.env['res.country.state'].search([('name', 'ilike', 'Quebec%')]).id,
|
||||
'country_id': cls.env['res.country'].search([('name', '=', 'Canada')]).id
|
||||
})
|
||||
|
||||
cls.partner_contact = cls.env['res.partner'].create({
|
||||
'name': 'Site Contact',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner_company.id,
|
||||
})
|
||||
|
||||
cls.equipment = cls.env['bemade_fsm.equipment'].create({
|
||||
'name': 'Test Equipment 1',
|
||||
'partner_location_id': cls.partner_company.id,
|
||||
})
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEquipmentBase(TestEquipmentCommon):
|
||||
|
||||
def test_crd(self):
|
||||
# Just make sure the basic ORM stuff is OK
|
||||
self.assertTrue(self.equipment in self.partner_company.equipment_ids)
|
||||
self.assertTrue(len(self.partner_company.equipment_ids) == 1)
|
||||
self.partner_company.write({'equipment_ids': [Command.set([])]})
|
||||
self.assertTrue(equipment in partner_company.equipment_ids)
|
||||
self.assertTrue(len(partner_company.equipment_ids) == 1)
|
||||
|
||||
# Delete should cascade
|
||||
partner_company.write({'equipment_ids': [Command.set([])]})
|
||||
with self.assertRaises(MissingError):
|
||||
self.equipment.name
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEquipmentTours(HttpCase, TestEquipmentCommon):
|
||||
|
||||
def test_equipment_base_tour(self):
|
||||
self.start_tour('/web', 'equipment_base_tour',
|
||||
login=self.user.login, )
|
||||
|
||||
def test_equipment_sale_order_tour(self):
|
||||
self.start_tour('/web', 'equipment_sale_order_tour', login=self.user.login)
|
||||
equipment.name
|
||||
|
|
|
|||
|
|
@ -1,56 +1,89 @@
|
|||
from odoo.tests import TransactionCase, HttpCase, tagged
|
||||
from odoo.tests import TransactionCase, HttpCase, tagged, Form
|
||||
from odoo import Command
|
||||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
Partner = cls.env['res.partner']
|
||||
cls.parent_co = Partner.create({
|
||||
'name': 'Parent Co',
|
||||
'company_type': 'company',
|
||||
})
|
||||
cls.contact_1 = Partner.create({
|
||||
'name': 'Contact 1',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.parent_co.id,
|
||||
})
|
||||
cls.contact_2 = Partner.create({
|
||||
'name': 'Contact 2',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.parent_co.id,
|
||||
})
|
||||
class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
|
||||
|
||||
def test_site_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
contact_1 = self._generate_partner('Contact 1', 'person', parent_co)
|
||||
contact_2 = self._generate_partner('Contact 2', 'person', parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
self.parent_co.write({'site_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.parent_co.id,
|
||||
})
|
||||
self.assertTrue(so.site_contacts == self.parent_co.site_contacts)
|
||||
parent_co.write({'site_contacts': [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(so.site_contacts == parent_co.site_contacts)
|
||||
|
||||
# Make sure updating the site contacts on the SO doesn't feed back to the partner
|
||||
so.write({'site_contacts': [Command.set([self.contact_1.id])]})
|
||||
self.assertTrue(self.contact_1 in so.site_contacts)
|
||||
self.assertTrue(self.contact_2 not in so.site_contacts)
|
||||
so.write({'site_contacts': [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.site_contacts)
|
||||
self.assertTrue(contact_2 not in so.site_contacts)
|
||||
self.assertTrue(so.site_contacts != so.partner_id.site_contacts and len(so.partner_id.site_contacts) == 2)
|
||||
|
||||
def test_default_workorder_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
contact_1 = self._generate_partner('Contact 1', 'person', parent_co)
|
||||
contact_2 = self._generate_partner('Contact 2', 'person', parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
self.parent_co.write({'work_order_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.parent_co.id,
|
||||
})
|
||||
self.assertTrue(self.contact_1 in self.parent_co.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 in self.parent_co.work_order_contacts)
|
||||
self.assertTrue(self.contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 in so.work_order_contacts)
|
||||
parent_co.write({'work_order_contacts': [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(contact_1 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_2 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 in so.work_order_contacts)
|
||||
|
||||
# Make sure setting the work order contacts on the SO doesn't feed back to the partner
|
||||
so.write({'work_order_contacts': [Command.set([self.contact_1.id])]})
|
||||
self.assertTrue(self.contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 not in so.work_order_contacts)
|
||||
so.write({'work_order_contacts': [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 not in so.work_order_contacts)
|
||||
self.assertTrue(
|
||||
so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2)
|
||||
|
||||
def test_multilayer_site_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery')
|
||||
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location)
|
||||
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location)
|
||||
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location)
|
||||
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location)
|
||||
shipping_location.write({
|
||||
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])],
|
||||
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])]
|
||||
})
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
so.write({'partner_shipping_id': shipping_location.id})
|
||||
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
|
||||
def test_onchange_shipping_address(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery')
|
||||
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location)
|
||||
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location)
|
||||
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location)
|
||||
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location)
|
||||
shipping_location.write({
|
||||
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])],
|
||||
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])]
|
||||
})
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
|
||||
# Set back to a location without site or work order contacts
|
||||
form = Form(so)
|
||||
form.partner_shipping_id = parent_co
|
||||
form.save()
|
||||
# Make sure the contacts were reset on the SO
|
||||
self.assertFalse(so.work_order_contacts)
|
||||
self.assertFalse(so.site_contacts)
|
||||
|
||||
# Now set back to the location with the FSM contacts and make sure they get set on the SO
|
||||
form.partner_shipping_id = shipping_location
|
||||
form.save()
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
|
|
|
|||
124
bemade_fsm/tests/test_fsm_visit.py
Normal file
124
bemade_fsm/tests/test_fsm_visit.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
from odoo.tests import TransactionCase, tagged, Form
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class FSMVisitTest(BemadeFSMBaseTest):
|
||||
|
||||
def test_create_visit_sets_name_on_section(self):
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
visit = self._generate_visit(so)
|
||||
|
||||
self.assertTrue(visit.so_section_id)
|
||||
self.assertEqual(visit.so_section_id.name, visit.label)
|
||||
|
||||
def test_change_visit_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
line.name = "Second Label"
|
||||
|
||||
self.assertEqual(visit.label, "Second Label")
|
||||
|
||||
def test_change_visit_label_changes_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
visit.label = "Second Label"
|
||||
|
||||
self.assertEqual(line.name, "Second Label")
|
||||
|
||||
def test_visit_completes_when_task_completes(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda l: l.task_id).task_id
|
||||
|
||||
task.action_fsm_validate()
|
||||
|
||||
self.assertTrue(visit.is_completed)
|
||||
|
||||
def test_visit_shows_invoiced_when_invoiced(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda l: l.task_id).task_id
|
||||
task.action_fsm_validate()
|
||||
|
||||
self._invoice_sale_order(so)
|
||||
|
||||
self.assertTrue(visit.is_invoiced)
|
||||
|
||||
def test_visit_groups_section_tasks_when_confirmed(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertTrue(visit_task)
|
||||
visit_subtasks = visit_task.child_ids
|
||||
self.assertTrue(
|
||||
visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks)
|
||||
|
||||
def test_visit_task_gets_correct_due_date_on_confirmation(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertEqual(visit_task.date_deadline, visit.approx_date)
|
||||
|
||||
def test_visit_task_gets_correct_duration_on_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
visit.approx_date = date.today() + timedelta(days=7)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertEqual(visit_task.planned_hours, 8.0)
|
||||
|
||||
def test_adding_visit_creates_one_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
self._generate_visit(sale_order=so)
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
def test_marking_visit_task_done_completes_descendants(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
so.action_confirm()
|
||||
parent, child1, child2 = visit.task_id, sol1.task_id, sol2.task_id
|
||||
|
||||
parent.action_fsm_validate()
|
||||
|
||||
self.assertTrue(parent.is_closed)
|
||||
self.assertTrue(child1.is_closed)
|
||||
self.assertTrue(child2.is_closed)
|
||||
self.assertEqual(sol1.qty_to_deliver, 0)
|
||||
self.assertEqual(sol2.qty_to_deliver, 0)
|
||||
self.assertTrue(visit.is_completed)
|
||||
|
||||
def test_marking_visit_task_done_does_not_create_sale_order_line(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
so.action_confirm()
|
||||
|
||||
visit.task_id.action_fsm_validate()
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
|
@ -1,97 +1,272 @@
|
|||
from .test_task_template import TestTaskTemplateCommon
|
||||
from .test_equipment import TestEquipmentCommon
|
||||
from odoo.tests.common import tagged
|
||||
from .test_task_template import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, HttpCase, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSalesOrder(TestTaskTemplateCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Test Partner',
|
||||
})
|
||||
cls.sale_order1 = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
'client_order_ref': 'TEST ORDER',
|
||||
})
|
||||
cls.sol_serv_order = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_global_project.name,
|
||||
'product_id': cls.product_task_global_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_global_project.uom_id.id,
|
||||
'price_unit': 120.0,
|
||||
'order_id': cls.sale_order1.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_in_project.name,
|
||||
'product_id': cls.product_task_in_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_in_project.uom_id.id,
|
||||
'price_unit': 150.0,
|
||||
'order_id': cls.sale_order1.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sale_order2 = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
'client_order_ref': 'TEST ORDER',
|
||||
})
|
||||
cls.sol_tree_order = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_tree_global_project.name,
|
||||
'product_id': cls.product_task_tree_global_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_tree_global_project.uom_id.id,
|
||||
'price_unit': 120.0,
|
||||
'order_id': cls.sale_order2.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_tree_in_project.name,
|
||||
'product_id': cls.product_task_tree_in_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_tree_in_project.uom_id.id,
|
||||
'price_unit': 150.0,
|
||||
'order_id': cls.sale_order2.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
|
||||
class TestSalesOrder(BemadeFSMBaseTest):
|
||||
@tagged('-at_install', 'post_install')
|
||||
def test_order_confirmation_simple_template(self):
|
||||
""" Confirming the order should create a task in the global project. """
|
||||
so = self.sale_order1
|
||||
""" Confirming the order should create a task in the global project based on the task template. """
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
sol1 = so.order_line[0]
|
||||
sol2 = so.order_line[1]
|
||||
|
||||
task = sol.task_id
|
||||
self.assertTrue(task)
|
||||
self.assertTrue(task_template.name in task.name)
|
||||
self.assertTrue(task_template.planned_hours == task.planned_hours)
|
||||
|
||||
def test_task_template_tree_order_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
parent_template = self._generate_task_template(structure=[2, 1],
|
||||
names=['Parent Template',
|
||||
'Child Template',
|
||||
'Grandchild Template'])
|
||||
child_template_1 = parent_template.subtasks[0]
|
||||
child_template_2 = parent_template.subtasks[1]
|
||||
grandchild_template = parent_template.subtasks[0].subtasks[0]
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
parent = sol.task_id
|
||||
c1, c2 = parent.child_ids
|
||||
gc = c1.child_ids[:1]
|
||||
self.assertTrue(parent.child_ids and len(parent.child_ids) == 2)
|
||||
self.assertTrue(parent_template.name in parent.name)
|
||||
self.assertEqual(child_template_1.name, c1.name)
|
||||
self.assertEqual(child_template_2.name, c2.name)
|
||||
self.assertTrue(c1.child_ids and len(c1.child_ids) == 1)
|
||||
self.assertEqual(grandchild_template.name, gc.name)
|
||||
|
||||
def test_order_confirmation_single_equipment(self):
|
||||
""" The equipment selected on the SO should transfer to the task."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner, equipment=equipment)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product1 = self._generate_product(task_template=task_template)
|
||||
product2 = self._generate_product()
|
||||
sol1 = self._generate_sale_order_line(so, product=product1)
|
||||
sol2 = self._generate_sale_order_line(so, product=product2)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task1 = sol1.task_id
|
||||
task2 = sol2.task_id
|
||||
self.assertTrue(task1)
|
||||
self.assertTrue(task2)
|
||||
self.assertTrue(self.task1.name in task1.name)
|
||||
self.assertTrue(self.task1.name in task2.name)
|
||||
self.assertTrue(self.task1.planned_hours == task1.planned_hours)
|
||||
self.assertEqual(task1.equipment_ids[0], equipment)
|
||||
self.assertEqual(task2.equipment_ids[0], equipment)
|
||||
|
||||
def test_order_confirmation_tree_template(self):
|
||||
def assert_structure(sol):
|
||||
self.assertTrue(sol.task_id.child_ids and len(sol.task_id.child_ids) == 2)
|
||||
self.assertTrue(self.parent_task.name in sol.task_id.name)
|
||||
self.assertTrue(self.child_task_1.name in sol.task_id.child_ids[0].name)
|
||||
self.assertTrue(self.child_task_2.name in sol.task_id.child_ids[1].name)
|
||||
self.assertTrue(sol.task_id.child_ids[1].child_ids and len(sol.task_id.child_ids.child_ids) == 1)
|
||||
self.assertTrue(self.grandchild_task.name in sol.task_id.child_ids.child_ids[0].name)
|
||||
def test_order_confirmation_multiple_equipment(self):
|
||||
""" All equipment items should flow from the sale order line to the final task """
|
||||
partner = self._generate_partner()
|
||||
for i in range(5):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(
|
||||
partner=partner) # No default equipment since more than 3 on partner
|
||||
sol1, sol2, sol3 = [self._generate_sale_order_line(sale_order=sale_order) for i
|
||||
in range(3)]
|
||||
sol1.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2)])]
|
||||
sol3.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2, 5)])]
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.equipment_ids, sol1.task_id.equipment_ids)
|
||||
self.assertEqual(sol2.equipment_ids, sol2.task_id.equipment_ids)
|
||||
self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids)
|
||||
|
||||
def test_task_template_with_equipment_flow(self):
|
||||
""" The equipment selected on a task template should flow down to the task created on SO confirmation."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(equipment=equipment)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so = self.sale_order2
|
||||
so.action_confirm()
|
||||
sol1 = so.order_line[0]
|
||||
sol2 = so.order_line[1]
|
||||
assert_structure(sol1)
|
||||
assert_structure(sol2)
|
||||
|
||||
def test_order_confirmation_equipment(self):
|
||||
so = self.sale_order1
|
||||
equipment = self.env['bemade_fsm.equipment'].create(
|
||||
{'name': 'test equipment', 'partner_location_id': so.partner_shipping_id.id})
|
||||
so.equipment_id = equipment.id
|
||||
self.assertEqual(sol.task_id.equipment_ids[0], equipment)
|
||||
|
||||
def test_sale_order_line_gets_default_equipment(self):
|
||||
""" Sale order lines created on an SO with default equipment set should inherit that default equipment. """
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
self.assertEqual(sol.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_sale_order_gets_correct_default_equipment_from_partner(self):
|
||||
""" Should pick up equipment from the partner."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, partner.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_default_equipment_maximum_number(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(3):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(4):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_resets_default_equipment_on_partner_change(self):
|
||||
partner_1 = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
self._generate_equipment(partner=partner_1)
|
||||
sale_order = self._generate_sale_order(partner_1)
|
||||
form = Form(sale_order)
|
||||
|
||||
form.partner_id = partner_2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(sale_order.default_equipment_ids)
|
||||
|
||||
def test_sale_order_prioritize_shipping_location_equipments(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent, location_type='delivery')
|
||||
self._generate_equipment(partner=parent)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent, shipping_location=child)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, child.equipment_ids)
|
||||
|
||||
def test_default_equipment_transfers_to_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
for i in range(3):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
for i in range(3):
|
||||
self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
for line in sale_order.order_line:
|
||||
self.assertEqual(line.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_task_mark_done(self):
|
||||
""" Marking the task linked to an SO line should mark the line delivered. Marking sub-tasks done should not."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(structure=[2],
|
||||
names=["Parent Task", "Subtask"])
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
task = so.order_line[0].task_id
|
||||
self.assertTrue(task.equipment_id == equipment)
|
||||
parent_task = sol.task_id
|
||||
subtasks = parent_task._get_all_subtasks()
|
||||
|
||||
# Marking the subtasks done should not increment delivered quantity
|
||||
subtasks.action_fsm_validate()
|
||||
self.assertEqual(sol.qty_delivered, 0)
|
||||
|
||||
# Marking the top-level tasks done should set the delivered quantity to some non-zero value based on the UOM
|
||||
parent_task.action_fsm_validate()
|
||||
self.assertTrue(sol.qty_delivered != 0)
|
||||
|
||||
def test_task_contacts_through_sale_order(self):
|
||||
""" Make sure the site contacts and work order contacts transfer correctly from the SO to the task."""
|
||||
|
||||
partner = self._generate_partner()
|
||||
contact1 = self._generate_partner('Site contact', 'person', partner)
|
||||
contact2 = self._generate_partner('Work order contact', 'person', partner)
|
||||
partner.write({
|
||||
'site_contacts': [Command.set([contact1.id])],
|
||||
'work_order_contacts': [Command.set([contact2.id])],
|
||||
})
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(so.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, partner.site_contacts)
|
||||
self.assertEqual(sol.task_id.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(sol.task_id.site_contacts, partner.site_contacts)
|
||||
|
||||
def test_changing_task_contacts_mirrors_with_sale_order(self):
|
||||
partner = self._generate_partner()
|
||||
contact = self._generate_partner("Contact", "person", partner)
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(so, product)
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task_form = Form(task)
|
||||
|
||||
# Now change the site/work order contact on the task and make sure it feeds back to the sales order
|
||||
task_form.site_contacts.add(contact)
|
||||
task_form.work_order_contacts.add(contact)
|
||||
task_form.save()
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
# Test changing it on the SO feeds back to the task as well
|
||||
f = Form(so)
|
||||
f.work_order_contacts.remove(contact.id)
|
||||
f.site_contacts.remove(contact.id)
|
||||
f.save()
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
def test_tasks_created_at_order_confirmation_have_no_assignees(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
user = self._generate_project_user(name="User", login='login')
|
||||
|
||||
# We test as a specific user since testing as root may not produce the error
|
||||
so.with_user(user).action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
subtask1 = visit_task.child_ids[0]
|
||||
subtask2 = visit_task.child_ids[1]
|
||||
self.assertFalse(visit_task.user_ids)
|
||||
self.assertFalse(subtask1.user_ids)
|
||||
self.assertFalse(subtask2.user_ids)
|
||||
|
||||
def test_long_line_name_overflows_to_task_description(self):
|
||||
so = self._generate_sale_order()
|
||||
product = self._generate_product()
|
||||
product.description_sale = "This is a long product description.\n" \
|
||||
"It even spans multiple lines.\n" \
|
||||
"One could find this annoying in a task name."
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertFalse("This is a long product description." in task.name)
|
||||
self.assertFalse("It even spans multiple lines." in task.name)
|
||||
self.assertFalse("One could find this annoying in a task name." in task.name)
|
||||
self.assertTrue("It even spans multiple lines." in task.description)
|
||||
self.assertTrue("One could find this annoying in a task name."
|
||||
in task.description)
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
from .test_sale_order import TestSalesOrder
|
||||
from odoo import Command
|
||||
from odoo.tests.common import Form
|
||||
|
||||
|
||||
class TestSaleOrderTaskContacts(TestSalesOrder):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
Partner = cls.env['res.partner']
|
||||
cls.partner2 = Partner.create({
|
||||
'name': 'New Partner',
|
||||
'company_type': 'company',
|
||||
})
|
||||
cls.contact = Partner.create({
|
||||
'name': 'Contact',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner2.id,
|
||||
})
|
||||
cls.contact2 = cls.env['res.partner'].create({
|
||||
'name': 'Contact 2',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner2.id,
|
||||
})
|
||||
|
||||
def _test_task_contacts_from_so(self, field):
|
||||
""" Shorthand function for testing both work_order_contacts and site_contacts fields on SOs and tasks."""
|
||||
def ga(obj):
|
||||
return getattr(obj, field)
|
||||
# Add the default site/work order contact to the partner, create an SO with the partner
|
||||
self.partner2.write({field: [Command.set([self.contact.id])]})
|
||||
self.sale_order1.write(
|
||||
{'partner_id': self.partner2.id, field: [Command.set(ga(self.partner2).ids)]})
|
||||
so = self.sale_order1
|
||||
self.assertTrue(ga(so))
|
||||
# Confirm the SO and check that the task got the default carried over
|
||||
so.action_confirm()
|
||||
task = so.order_line[0].task_id
|
||||
self.assertTrue(ga(task))
|
||||
self.assertTrue(ga(task) == ga(so))
|
||||
# Now change the site/work order contact on the task and make sure it feeds back to the sales order
|
||||
f = Form(task)
|
||||
ga(f).add(self.contact2)
|
||||
f.save()
|
||||
self.assertTrue(ga(so) == ga(task))
|
||||
# Test changing it on the SO feeds back to the task as well
|
||||
f = Form(so)
|
||||
ga(f).remove(self.contact.id)
|
||||
f.save()
|
||||
self.assertTrue(ga(so) == ga(task))
|
||||
|
||||
def test_work_order_contacts_on_task(self):
|
||||
# Make sure work order contacts on the sales order transfer to the task on order confirmation
|
||||
self._test_task_contacts_from_so('work_order_contacts')
|
||||
|
||||
def test_site_contacts_on_task(self):
|
||||
# Make sure site contacts from the sales order transfer to the task on order confirmation
|
||||
self._test_task_contacts_from_so('site_contacts')
|
||||
|
|
@ -1,109 +1,80 @@
|
|||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import HttpCase, tagged, Form
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo import Command
|
||||
from odoo.tools import mute_logger
|
||||
from psycopg2.errors import ForeignKeyViolation
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.PLANNED_HOURS = 6
|
||||
hours_uom = cls.env['uom.uom'].search([('name', '=', 'Hour')]) or False
|
||||
# Test product to use with the various tests
|
||||
cls.task1 = cls.env['project.task.template'].create({
|
||||
'name': 'Template 1',
|
||||
})
|
||||
|
||||
cls.project = cls.env['project.project'].create({
|
||||
'name': 'Test Project',
|
||||
})
|
||||
cls.product_task_global_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 1',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project.id,
|
||||
'task_template_id': cls.task1.id,
|
||||
'uom_id': hours_uom.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
})
|
||||
cls.project_template = cls.env['project.project'].create({
|
||||
'name': 'Test Project Template',
|
||||
})
|
||||
cls.product_task_in_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 2',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_in_project',
|
||||
'task_template_id': cls.task1.id,
|
||||
'project_template_id': cls.project_template.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
'uom_id': hours_uom.id,
|
||||
})
|
||||
|
||||
# Set up a task template tree with 2 children and 1 grandchild
|
||||
cls.parent_task = cls.env['project.task.template'].create({
|
||||
'name': 'Parent Template',
|
||||
'planned_hours': cls.PLANNED_HOURS,
|
||||
})
|
||||
cls.child_task_1 = cls.env['project.task.template'].create({
|
||||
'name': 'Child Template 1',
|
||||
'parent': cls.parent_task.id,
|
||||
})
|
||||
cls.child_task_2 = cls.env['project.task.template'].create({
|
||||
'name': 'Child Template 2',
|
||||
'parent': cls.parent_task.id,
|
||||
})
|
||||
cls.parent_task.write({'subtasks': [Command.set([cls.child_task_1.id, cls.child_task_2.id])]})
|
||||
cls.grandchild_task = cls.env['project.task.template'].create({
|
||||
'name': 'Grandchild Template',
|
||||
'parent': cls.child_task_2.id
|
||||
})
|
||||
cls.child_task_2.write({'subtasks': [Command.set([cls.grandchild_task.id])]})
|
||||
|
||||
# Create products using the task tree we just created
|
||||
cls.product_task_tree_global_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 3',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project.id,
|
||||
'task_template_id': cls.parent_task.id,
|
||||
'uom_id': hours_uom.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
})
|
||||
cls.product_task_tree_in_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 2',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_in_project',
|
||||
'task_template_id': cls.parent_task.id,
|
||||
'project_template_id': cls.project_template.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
'uom_id': hours_uom.id,
|
||||
})
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskTemplate(TestTaskTemplateCommon):
|
||||
class TestTaskTemplate(BemadeFSMBaseTest):
|
||||
|
||||
def test_delete_task_template(self):
|
||||
"""User should never be able to delete a task template used on a product"""
|
||||
task_template = self._generate_task_template(names=['Template 1'])
|
||||
product = self._generate_product(name="Test Product 1", task_template=task_template)
|
||||
with self.assertRaises(ForeignKeyViolation):
|
||||
self.task1.unlink()
|
||||
with mute_logger('odoo.sql_db'):
|
||||
task_template.unlink()
|
||||
|
||||
def test_delete_subtask_template(self):
|
||||
""" Deletion of a child task should be OK even if the parent is on a product. Children of the deleted
|
||||
subtask should be deleted."""
|
||||
self.child_task_2.unlink()
|
||||
parent_task = self._generate_task_template(structure=[2, 1],
|
||||
names=['Parent Template', 'Child Template',
|
||||
'Grandchild Template'])
|
||||
grandchild_task = parent_task.subtasks[0].subtasks[0]
|
||||
|
||||
parent_task.subtasks[0].unlink()
|
||||
|
||||
# Reading deleted child's name field should be impossible
|
||||
with self.assertRaises(MissingError):
|
||||
test = self.grandchild_task.name
|
||||
test = grandchild_task.name
|
||||
|
||||
def test_dissociating_customer_resets_equipment_appropriately(self):
|
||||
partner1 = self._generate_partner()
|
||||
partner2 = self._generate_partner()
|
||||
equipment1 = self._generate_equipment(partner=partner1)
|
||||
task = self._generate_task_template(customer=partner1, equipment=equipment1)
|
||||
form = Form(task)
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskTemplateTour(HttpCase, TestTaskTemplateCommon):
|
||||
# Switching the partner should trigger on_change that makes sure equipments are linked to the new partner
|
||||
form.customer = partner2
|
||||
form.save()
|
||||
|
||||
def test_task_template_tour(self):
|
||||
self.start_tour('/web', 'task_template_tour',
|
||||
login='misterpm', )
|
||||
self.assertFalse(equipment1 in task.equipment_ids)
|
||||
|
||||
def test_hours_estimate_used_for_planning(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(uom=self.env.ref('uom.product_uom_unit'), task_template=task_template)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
self.assertEqual(sol.task_duration, 8)
|
||||
|
||||
def test_hours_estimate_multiplied_for_multiple_units_sold(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(uom=self.env.ref('uom.product_uom_unit'),
|
||||
task_template=task_template)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product, qty=3.0)
|
||||
|
||||
self.assertEqual(sol.task_duration, 24)
|
||||
|
||||
def test_child_task_names_are_short_version(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
template = self._generate_task_template(names=['Task'])
|
||||
product = self._generate_product(task_template=template)
|
||||
sol1.name = "Short Name 1"
|
||||
sol2.name = "Short Name 2"
|
||||
sol3 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.task_id.name, "Short Name 1")
|
||||
self.assertEqual(sol2.task_id.name, "Short Name 2")
|
||||
self.assertEqual(sol3.task_id.name, "Task")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
<field name="location_notes"/>
|
||||
</group>
|
||||
<group name="right">
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_location_id"
|
||||
groups="sale.group_delivery_invoice_address"
|
||||
context="{'default_type': 'delivery', 'show_address': 1}"
|
||||
|
|
@ -23,19 +22,6 @@
|
|||
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
|
||||
</group>
|
||||
</group>
|
||||
<!-- <notebook>-->
|
||||
<!-- <page string="Interventions">-->
|
||||
<!-- <field name="intervention_ids" mode="tree">-->
|
||||
<!-- <tree string="Interventions" create="0" edit="0">-->
|
||||
<!-- <field name="name"/>-->
|
||||
<!-- <field name="description"/>-->
|
||||
<!-- <field name="date_planned"/>-->
|
||||
<!-- <field name="state"/>-->
|
||||
<!-- </tree>-->
|
||||
<!-- </field>-->
|
||||
<!-- </page>-->
|
||||
<!-- </notebook>-->
|
||||
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
|
|
@ -50,36 +36,16 @@
|
|||
<field name="name">bemade_fsm.equipment.tree</field>
|
||||
<field name="model">bemade_fsm.equipment</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Equipment">
|
||||
<tree string="Equipment" editable="bottom">
|
||||
<field name="pid_tag"/>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_location_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- BV: AN OTHER SEARCH VIEW TO BRING BACK
|
||||
<record id="equipment_view_search" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.equipment.search</field>
|
||||
<field name="model">bemade_fsm.equipment</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Equipments">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="pid_tag"/>
|
||||
<field name="partner_location_id"/>
|
||||
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Partner" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Location" context="{'group_by':'partner_location_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record model="ir.actions.act_window" id="action_window_equipment">
|
||||
<field name="name">Equipment</field>
|
||||
<field name="res_model">bemade_fsm.equipment</field>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<field name="task_template_id"
|
||||
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='planning_enabled']" position="after">
|
||||
<field name="is_field_service"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">
|
||||
|
|
|
|||
|
|
@ -8,17 +8,45 @@
|
|||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='other_information']" position="before">
|
||||
<page name="field_service" string="Field Service">
|
||||
<group name="field_service_info">
|
||||
<field name="equipment_id"
|
||||
domain="[('partner_location_id', '=', partner_shipping_id)]"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"/>
|
||||
<group name="fsm_visits" string="Service Visits">
|
||||
<field name="visit_ids"
|
||||
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"/>
|
||||
</group>
|
||||
<group name="field_service_info" string="Contacts and Equipment">
|
||||
<field name="valid_equipment_ids" invisible="1"/>
|
||||
<field name="summary_equipment_ids"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"/>
|
||||
<field name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
|
||||
<field name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
|
||||
|
||||
<field name="default_equipment_ids"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//tree//field[@name='name']" position="after">
|
||||
<field name="valid_equipment_ids" invisible="1"/>
|
||||
<field name="equipment_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="bemade_fsm_visit_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.tree</field>
|
||||
<field name="model">bemade_fsm.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="label"/>
|
||||
<field name="approx_date"/>
|
||||
<field name="is_completed" widget="boolean"/>
|
||||
<field name="is_invoiced" widget="boolean"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
</group>
|
||||
<group>
|
||||
<field name="customer"/>
|
||||
<field name="equipment_ids"
|
||||
domain="[('partner_location_id', '=', customer)]"
|
||||
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
|
||||
<field name="tags" widget="many2many_tags"/>
|
||||
<field name="company_id" />
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,21 @@
|
|||
<record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="inherit_id" ref="industry_fsm.view_task_form2_inherit"/>
|
||||
<field name="priority" eval="8"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="equipment_id" domain="[('partner_location_id', '=', partner_id)]"/>
|
||||
<xpath expr="//field[@name='name']/.." position="before">
|
||||
<h1 class="d-flex flex-row justify-content-between">
|
||||
<field name="work_order_number"
|
||||
attrs="{'invisible': [('work_order_number', '=', False)]}"/>
|
||||
</h1>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='extra_info']" position="after">
|
||||
<page string="Field Service" name="field_service">
|
||||
<group name="contacts">
|
||||
<page string="Field Service" name="field_service" translate="True">
|
||||
<group name="equipment_and_contacts">
|
||||
<field name="equipment_ids"
|
||||
domain="[('partner_location_id', '=', partner_id)]"
|
||||
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
|
||||
<field name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
|
||||
<field name="work_order_contacts"
|
||||
|
|
@ -20,6 +26,16 @@
|
|||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<button name="action_fsm_validate" class='btn-primary'
|
||||
position="attributes">
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
<button name="action_fsm_validate" class='btn-secondary'
|
||||
position="attributes">
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
|
||||
|
|
@ -29,48 +45,79 @@
|
|||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_map" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_map"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_to_schedule_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_to_schedule_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_all_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_all_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm_sale.project_task_action_to_invoice_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm_sale.project_task_action_to_invoice_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for planning actions as well -->
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_user" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_user"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_project" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_project"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet" model="ir.actions.act_window">
|
||||
<record id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="project_task_view_calendar_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.project_task_view_calendar_fsm</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm.project_task_view_calendar_fsm"/>-->
|
||||
<!-- <field name="model">project.task</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <field name="user_ids" position="attributes">-->
|
||||
<!-- <attribute name="filters">1</attribute>-->
|
||||
<!-- <attribute name="color">user_ids</attribute>-->
|
||||
<!-- </field>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
<record id="project_task_view_calendar_fsm_no_worksheet" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task_view_calendar_no_worksheet</field>
|
||||
<field name="inherit_id"
|
||||
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"/>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='worksheet_template_id']"
|
||||
position="replace"></xpath>
|
||||
<xpath expr="//calendar" position="attributes">
|
||||
<attribute name="color">user_ids</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
1
bemade_geo_routing/__init__.py
Normal file
1
bemade_geo_routing/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
35
bemade_geo_routing/__manifest__.py
Normal file
35
bemade_geo_routing/__manifest__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Bemade Geo Distance',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Utility module for calculating the distance driving between addresses.',
|
||||
'description': """Module for calculating the driving distance between addresses using the Google Route API.
|
||||
**IMPORTANT NOTE:** You must have an active Google Route API key configured in the Geo Localization
|
||||
settings (under General Settings).""",
|
||||
'category': 'Generic Modules/Others',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['base_geolocalize'],
|
||||
'data': [],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_geo_routing/models/__init__.py
Normal file
1
bemade_geo_routing/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import bemade_geo_router
|
||||
95
bemade_geo_routing/models/bemade_geo_router.py
Normal file
95
bemade_geo_routing/models/bemade_geo_router.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from odoo import models, fields, tools, _, api
|
||||
from odoo.exceptions import UserError
|
||||
import requests
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Literal
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeoRouter(models.AbstractModel):
|
||||
""" Abstract class used to call Google Route API and convert responses
|
||||
into useful distance and route data.
|
||||
"""
|
||||
_name = 'base.geo_router'
|
||||
_description = "Geographic Router"
|
||||
|
||||
units = Literal['metric', 'imperial']
|
||||
|
||||
@api.model
|
||||
def _get_api_key(self):
|
||||
apikey = self.env['ir.config_parameter'].sudo().get_param('base_geolocalize.google_map_api_key')
|
||||
if not apikey:
|
||||
raise UserError(_(
|
||||
"API key for GeoCoding (Places) required.\n"
|
||||
"Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information."
|
||||
))
|
||||
return apikey
|
||||
|
||||
@api.model
|
||||
def get_driving_distance_time(self, origin, destination, departure_time: datetime.datetime = None,
|
||||
arrival_time: datetime.datetime = None, units: units = 'metric',
|
||||
avoid_tolls=False, avoid_highways=False, avoid_ferries=False) -> (float, float):
|
||||
""" Calculates the route between two addresses and returns the distance it either in Kilometers or Miles and
|
||||
the time in minutes.
|
||||
|
||||
:param origin: The origin address (res.partner) from which to drive (origin).
|
||||
:param destination: The destination address (res.partner).
|
||||
:param departure_time: The departure time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with arrival_time.
|
||||
:param arrival_time: The arrival time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with departure_time.
|
||||
:param units: Type of units to return: 'imperial' for Miles, 'metric' for Kilometers.
|
||||
:param avoid_tolls: Whether Google should calculate a route avoiding tolls.
|
||||
:param avoid_highways: Whether Google should calculate a route avoiding highways.
|
||||
:param avoid_ferries: Whether Google should calculate a route avoiding ferries.
|
||||
:returns: tuple(distance: float, time: float)
|
||||
"""
|
||||
if departure_time and arrival_time:
|
||||
raise ValueError("Route cannot be calculated with both an arrival time and departure time.")
|
||||
apikey = self._get_api_key()
|
||||
origin_addr = self.env['base.geocoder'].geo_query_address(street=origin.street, zip=origin.zip,
|
||||
city=origin.city, state=origin.state_id.name,
|
||||
country=origin.country_id.name)
|
||||
destination_addr = self.env['base.geocoder'].geo_query_address(street=destination.street, zip=destination.zip,
|
||||
city=destination.city,
|
||||
state=destination.state_id.name,
|
||||
country=destination.country_id.name)
|
||||
params = {
|
||||
'origin': {
|
||||
'address': origin_addr,
|
||||
},
|
||||
'destination': {
|
||||
'address': destination_addr,
|
||||
},
|
||||
'travelMode': 'DRIVE',
|
||||
'computeAlternativeRoutes': False,
|
||||
'routeModifiers': {
|
||||
'avoidTolls': avoid_tolls,
|
||||
'avoidHighways': avoid_highways,
|
||||
'avoidFerries': avoid_ferries,
|
||||
},
|
||||
'languageCode': 'en-US',
|
||||
'units': units,
|
||||
}
|
||||
if departure_time or arrival_time:
|
||||
params.update({
|
||||
'routingPreference': 'TRAFFIC_AWARE',
|
||||
'departureTime' if departure_time else 'arrivalTime': departure_time or arrival_time
|
||||
})
|
||||
|
||||
url = "https://routes.googleapis.com/directions/v2:computeRoutes"
|
||||
distance_field = 'distanceMeters' if units == 'metric' else 'distanceMiles'
|
||||
result = requests.post(url,
|
||||
json=params,
|
||||
headers={'X-Goog-Api-Key': apikey,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-FieldMask': f"routes.duration,routes.{distance_field}"}).json()
|
||||
if 'routes' not in result:
|
||||
_logger.error(f"Failed to retrieve route between {origin_addr} and {destination_addr}. "
|
||||
f"Google response: {result}.")
|
||||
route = result['routes'][0]
|
||||
distance = (route[distance_field] / 1000) if units == 'metric' else route[distance_field]
|
||||
time = int(route['duration'].strip('s')) / 60
|
||||
return distance, time
|
||||
1
bemade_geo_routing/tests/__init__.py
Normal file
1
bemade_geo_routing/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_geo_router
|
||||
53
bemade_geo_routing/tests/test_geo_router.py
Normal file
53
bemade_geo_routing/tests/test_geo_router.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestGeoRouter(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.apikey = cls.env['base.geo_router']._get_api_key()
|
||||
|
||||
def test_distance_time_basic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal)
|
||||
|
||||
self.assertTrue(20 < distance < 40)
|
||||
self.assertTrue(12 < time < 60)
|
||||
|
||||
def test_time_traffic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
est = pytz.timezone('US/Eastern')
|
||||
today_naive = datetime.date.today()
|
||||
today = datetime.datetime(today_naive.year, today_naive.month, today_naive.day, 8, 30, tzinfo=est)
|
||||
arrival_time_local = today + datetime.timedelta(days=(2 - today.weekday()) % 7)
|
||||
arrival_time_utc = arrival_time_local.astimezone(pytz.utc)
|
||||
arrival_time = arrival_time_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
print(arrival_time)
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal,
|
||||
arrival_time=arrival_time)
|
||||
|
||||
self.assertTrue(25 < time < 90)
|
||||
|
||||
@classmethod
|
||||
def _generate_partners(self):
|
||||
bemade = self.env['res.partner'].create({
|
||||
'name': 'Bemade Inc.',
|
||||
'street': '255 North Montcalm Blvd.',
|
||||
'city': 'Candiac',
|
||||
'zip': 'J5R 3L6',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
montreal = self.env['res.partner'].create({
|
||||
'name': 'City of Montreal',
|
||||
'street': '220 East René-Lévesque Blvd.',
|
||||
'city': 'Montreal',
|
||||
'zip': 'H2X 3A7',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
return bemade, montreal
|
||||
1
bemade_planning_travel/__init__.py
Normal file
1
bemade_planning_travel/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
40
bemade_planning_travel/__manifest__.py
Normal file
40
bemade_planning_travel/__manifest__.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Planning Travel Time',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': "Plan travel time for services planned via Odoo's Planning module.",
|
||||
'description': """
|
||||
Adds the ability to add travel planning records to existing planning records
|
||||
linked to sale order lines. Travel planning records book time in the scheduled
|
||||
resource's calendar. Travel distance is based on the resource's work address, if
|
||||
available. Otherwise, travel is assumed to be between the client location and
|
||||
the company's address.""",
|
||||
'category': 'Services/Field Service',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['sale_planning',
|
||||
'industry_fsm',
|
||||
'bemade_geo_routing'],
|
||||
'data': [''],
|
||||
'demo': [''],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_planning_travel/models/__init__.py
Normal file
1
bemade_planning_travel/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import planning_slot
|
||||
42
bemade_planning_travel/models/planning_slot.py
Normal file
42
bemade_planning_travel/models/planning_slot.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class PlanningSlot(models.Model):
|
||||
_inherit = "planning.slot"
|
||||
|
||||
inbound_travel_slot = fields.Many2one(comodel_name="planning.slot",
|
||||
string="Inbound Travel Plan", )
|
||||
outbound_travel_slot = fields.Many2one(comodel_name="planning.slot",
|
||||
string="Outbound Travel Plan")
|
||||
|
||||
# Fields for travel slots
|
||||
travel_origin_slot = fields.One2many(comodel_name="planning.slot",
|
||||
inverse_name="outbound_travel_slot")
|
||||
travel_destination_slot = fields.One2many(comodel_name="planning.slot",
|
||||
inverse_name="inbound_travel_slot")
|
||||
is_travel_slot = fields.Boolean()
|
||||
|
||||
def action_plan_travel(self):
|
||||
# Get previous and next planning records for the same day, same resource
|
||||
for rec in self:
|
||||
prev_slot = rec._get_previous_same_day_same_resource_slot()
|
||||
next_slot = rec._get_next_same_day_same_resource_slot()
|
||||
|
||||
|
||||
def _get_previous_same_day_same_resource_slot(self):
|
||||
start = self.start_datetime.replace(hour=0, minute=0, second=0,
|
||||
microsecond=0)
|
||||
domain = [('is_travel_slot', '=', False),
|
||||
('start_datetime', '>=', start),
|
||||
('end_datetime', '<=', self.start_datetime),
|
||||
('resource_id', '=', self.resource_id.id)]
|
||||
return self.env['planning.slot'].search(domain)
|
||||
|
||||
def _get_next_same_day_same_resource_slot(self):
|
||||
end = self.end_datetime.replace(hour=23, minute=59, second=59,
|
||||
microsecond=999999)
|
||||
domain = [('is_travel_slot', '=', False),
|
||||
('start_datetime', '>=', self.end_datetime),
|
||||
('end_datetime', '<=', end),
|
||||
('resource_id', '=', self.resource_id.id)]
|
||||
return self.env['planning.slot'].search(domain)
|
||||
Loading…
Reference in a new issue