for migration
This commit is contained in:
parent
0b9e462831
commit
fb301db2b3
206 changed files with 8229 additions and 613 deletions
|
|
@ -1,2 +1,2 @@
|
|||
# bemade-addons Branch 16.0
|
||||
# bemade-addons
|
||||
Odoo Addons made by Bemade
|
||||
|
|
|
|||
2
bemade_documents_portal/__init__.py
Normal file
2
bemade_documents_portal/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
32
bemade_documents_portal/__manifest__.py
Normal file
32
bemade_documents_portal/__manifest__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 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': 'Documents Portal Base',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Adds documents to the front-end portal.',
|
||||
'category': 'Document Management',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['documents', 'portal', 'mail_enterprise', 'im_livechat'],
|
||||
'data': ['views/document_portal_templates.xml'],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_documents_portal/controllers/__init__.py
Normal file
1
bemade_documents_portal/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import portal
|
||||
65
bemade_documents_portal/controllers/portal.py
Normal file
65
bemade_documents_portal/controllers/portal.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.http import request, route
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo import _
|
||||
|
||||
|
||||
class DocumentCustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
rtn = super()._prepare_home_portal_values(counters)
|
||||
domain = self._prepare_documents_domain()
|
||||
rtn['documents_count'] = request.env['documents.document'].search_count(domain)
|
||||
return rtn
|
||||
|
||||
@route('/my/documents', type='http', auth='user', website=True)
|
||||
def portal_my_documents(self, **kwargs):
|
||||
values = self._prepare_portal_layout_values()
|
||||
Documents = request.env['documents.document']
|
||||
domain = self._prepare_documents_domain()
|
||||
documents_count = Documents.search_count(domain)
|
||||
documents = Documents.search(domain)
|
||||
values.update({
|
||||
'documents_count': documents_count,
|
||||
'documents': documents.sudo(),
|
||||
'default_url': '/my/documents',
|
||||
'page_name': 'my_documents',
|
||||
})
|
||||
return request.render("bemade_documents_portal.portal_my_documents", values)
|
||||
|
||||
def _prepare_documents_domain(self):
|
||||
partner = request.env.user.partner_id
|
||||
user = request.env.user
|
||||
"""Helper method intended to be overridden for future modules."""
|
||||
return ['|',
|
||||
('partner_id', '=', partner.id),
|
||||
('owner_id', '=', user.id),
|
||||
]
|
||||
|
||||
def _render_record_template(self, values):
|
||||
""" Override this method to apply a different template for a single document
|
||||
record on the portal. """
|
||||
return request.render("bemade_documents_portal.document_portal_template", values)
|
||||
|
||||
@route('/my/documents/<int:document_id>', type='http', auth='user', website=True)
|
||||
def portal_document_page(self, document_id, download=False, **kwargs):
|
||||
document = request.env['documents.document'].browse(document_id)
|
||||
if not document:
|
||||
raise MissingError(_('This document does not exist.'))
|
||||
if download:
|
||||
return self._download_attachment(document)
|
||||
values={
|
||||
'document': document,
|
||||
'page_name': 'my_documents',
|
||||
'action': document._get_portal_return_action(),
|
||||
}
|
||||
return self._render_record_template(values)
|
||||
|
||||
def _download_attachment(self, document):
|
||||
attachment = document.attachment_id
|
||||
headers = [
|
||||
('content-type', attachment.mimetype),
|
||||
('content-length', attachment.file_size),
|
||||
('content-disposition', f'attachment; filename="{document.name}"')
|
||||
]
|
||||
return request.make_response(attachment.raw, headers)
|
||||
1
bemade_documents_portal/models/__init__.py
Normal file
1
bemade_documents_portal/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import documents
|
||||
17
bemade_documents_portal/models/documents.py
Normal file
17
bemade_documents_portal/models/documents.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_name = 'documents.document'
|
||||
_inherit = ['documents.document', 'portal.mixin']
|
||||
|
||||
def _compute_access_url(self):
|
||||
super()._compute_access_url()
|
||||
for document in self:
|
||||
document.access_url = f'/my/documents/{document.id}'
|
||||
|
||||
def _get_portal_return_action(self):
|
||||
""" Return the action used to display documents when returning from customer
|
||||
portal."""
|
||||
self.ensure_one()
|
||||
return self.env.ref('documents.document_action')
|
||||
112
bemade_documents_portal/views/document_portal_templates.xml
Normal file
112
bemade_documents_portal/views/document_portal_templates.xml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="portal_my_home" inherit_id="portal.portal_my_home">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Documents</t>
|
||||
<t t-set="url">/my/documents</t>
|
||||
<t t-set="placeholder_count">documents_count</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="portal_my_documents" name="My Documents">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="documents" t-as="document">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-att-href="document.get_portal_url()">
|
||||
<t t-esc="document.name"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template id="document_portal_template" name="Document Portal Template">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="o_portal_fullwidth_alert"
|
||||
groups="documents.group_documents_user">
|
||||
<t t-call="portal.portal_back_in_edit_mode">
|
||||
<t t-set="backend_url"
|
||||
t-value="'/web#model=%s&id=%s&action=%s&view_type=form' % (document._name, document.id, action.id)"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<div class="row no-gutters">
|
||||
<h5 class="mb-1 mb-md-0">
|
||||
<span t-field="document.name"/>
|
||||
</h5>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<!-- Main Document Contents -->
|
||||
<div id="document_content"
|
||||
class="col-12 col-lg justify-content-end w-100 h-100">
|
||||
<div t-if="'image' in document.mimetype"
|
||||
class="o_attachment_preview_img">
|
||||
<img id="attachment_img"
|
||||
class="img img-fluid d-block"
|
||||
t-attf-src="/documents/content/{{document.id}}"/>
|
||||
</div>
|
||||
<iframe t-if="document.mimetype == 'application/pdf'"
|
||||
class="mb48 w-100 min-vh-100"
|
||||
t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/documents/content/{{document.id}}&filename={{document.name}}"/>
|
||||
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<a class="btn btn-secondary btn-block o_download_btn"
|
||||
t-att-href="document.get_portal_url(download=True)">
|
||||
Download</a>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Size:
|
||||
<t t-call="documents.format_file_size"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Type:
|
||||
<t t-esc="document.mimetype"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">Attachment Type:
|
||||
<t t-esc="document.attachment_type"/>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Chatter -->
|
||||
<div id="document_communication" class="card-body">
|
||||
<h2>History</h2>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="document"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="portal_breadcrumbs" inherit_id="portal.portal_breadcrumbs">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'my_documents'"
|
||||
t-attf-class="breadcrumb-item #{'active ' if not document else ''}">
|
||||
<a t-if="document"
|
||||
t-attf-href="/my/documents?{{ keep_query() }}">Documents</a>
|
||||
<t t-else="">Documents</t>
|
||||
</li>
|
||||
<li t-if="document" class="breadcrumb-item active" t-esc="document.name">
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Fetchmail Only on production environment",
|
||||
"version": "16.0.0.0.1",
|
||||
"version": "15.0.0.0.1",
|
||||
"category": "Extra Tools",
|
||||
'summary': 'Fetchmail Only on production environment',
|
||||
"description": """
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"author": "Bemade",
|
||||
'website': 'https://www.bemade.org',
|
||||
"depends": [
|
||||
'mail',
|
||||
'fetchmail',
|
||||
],
|
||||
"data": [
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
971
bemade_fsm/i18n/fr.po
Normal file
971
bemade_fsm/i18n/fr.po
Normal file
|
|
@ -0,0 +1,971 @@
|
|||
# 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-10-02 20:26+0000\n"
|
||||
"PO-Revision-Date: 2023-10-02 20:26+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 ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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.view_task_form2_inherit
|
||||
msgid "Description/Comments"
|
||||
msgstr "Description/Commentaires"
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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 been entirely "
|
||||
"delivered and invoiced."
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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
|
||||
#: 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 ventes"
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. module: bemade_fsm
|
||||
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__planned_date_begin
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#. 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
|
||||
#: 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"
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
from . import task_template
|
||||
from . import product_template
|
||||
from . import sale_order_line
|
||||
from . import sale_order
|
||||
from . import equipment
|
||||
from . import equipment_type
|
||||
from . import equipment_tag
|
||||
from . import task
|
||||
from . import res_partner
|
||||
from . import fsm_visit
|
||||
|
|
|
|||
|
|
@ -1,26 +1,6 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EquipmentTag(models.Model):
|
||||
_name = "bemade_fsm.equipment.tag"
|
||||
_description = 'Field service equipment category'
|
||||
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
color = fields.Integer('Color Index', default=10)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class EquipmentType(models.Model):
|
||||
_name = 'bemade_fsm.equipment.type'
|
||||
_description = 'Field service equipment type'
|
||||
_order = 'id'
|
||||
|
||||
name = fields.Char(string='Intervention Name', required=True, translate=True)
|
||||
|
||||
|
||||
class Equipment(models.Model):
|
||||
_name = 'bemade_fsm.equipment'
|
||||
_rec_name = 'complete_name'
|
||||
|
|
@ -33,44 +13,36 @@ class Equipment(models.Model):
|
|||
|
||||
complete_name = fields.Char(string="Equipment Name", compute="_compute_complete_name", store=True)
|
||||
|
||||
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",)
|
||||
tag_ids = fields.Many2many(
|
||||
comodel_name='bemade_fsm.equipment.tag',
|
||||
string='Application',
|
||||
help="Classify and analyze your equipment categories like: Boiler, Laboratory, Waste water, Pure water"
|
||||
)
|
||||
|
||||
description = fields.Text(string="Description",
|
||||
tracking=True)
|
||||
description = fields.Text(string="Description", tracking=True)
|
||||
|
||||
partner_location_id = fields.Many2one('res.partner',
|
||||
string="Physical Address",
|
||||
tracking=True,
|
||||
ondelete='cascade')
|
||||
partner_location_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string="Physical Address",
|
||||
tracking=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
location_notes = fields.Text(string="Physical Location Notes",
|
||||
tracking=True)
|
||||
task_ids = fields.One2many(comodel_name='project.task',
|
||||
inverse_name='equipment_id',
|
||||
string='Interventions')
|
||||
location_notes = fields.Text(string="Physical Location Notes", tracking=True)
|
||||
|
||||
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 +51,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 +67,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
|
||||
|
|
|
|||
13
bemade_fsm/models/equipment_tag.py
Normal file
13
bemade_fsm/models/equipment_tag.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EquipmentTag(models.Model):
|
||||
_name = "bemade_fsm.equipment.tag"
|
||||
_description = 'Field service equipment category'
|
||||
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
color = fields.Integer('Color Index', default=10)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
9
bemade_fsm/models/equipment_type.py
Normal file
9
bemade_fsm/models/equipment_type.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EquipmentType(models.Model):
|
||||
_name = 'bemade_fsm.equipment.type'
|
||||
_description = 'Field service equipment type'
|
||||
_order = 'id'
|
||||
|
||||
name = fields.Char(string='Intervention Name', required=True, translate=True)
|
||||
70
bemade_fsm/models/fsm_visit.py
Normal file
70
bemade_fsm/models/fsm_visit.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
|
|
@ -4,4 +4,13 @@ from odoo import fields, models, api
|
|||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict')
|
||||
task_template_id = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Task Template",
|
||||
ondelete='restrict'
|
||||
)
|
||||
|
||||
is_field_service = fields.Boolean(
|
||||
string="Plan as field service",
|
||||
help="Products planned as field service will have travel time considered in planning."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,46 +1,68 @@
|
|||
from odoo import api, fields, models, Command
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
equipment_count = fields.Integer(compute='_compute_equipment_count',
|
||||
string='Equipment Count')
|
||||
equipment_count = fields.Integer(compute='_compute_equipment_count', string='Equipment Count')
|
||||
|
||||
owned_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
inverse_name="partner_id",
|
||||
string="Owned Equipments")
|
||||
owned_equipment_ids = fields.One2many(
|
||||
comodel_name="bemade_fsm.equipment",
|
||||
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'
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
is_site_contact = fields.Boolean(string='Is a site contact',
|
||||
compute="_compute_is_site_contact",
|
||||
search="_search_is_site_contact",)
|
||||
site_ids = fields.Many2many(
|
||||
string='Work Sites',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_site_contact_rel',
|
||||
column1='site_contact_id',
|
||||
column2='site_id',
|
||||
tracking=True
|
||||
)
|
||||
|
||||
site_ids = fields.Many2many(string='Work Sites',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_site_contact_rel',
|
||||
column1='site_contact_id',
|
||||
column2='site_id',
|
||||
tracking=True)
|
||||
site_contacts = fields.Many2many(
|
||||
string='Site Contacts',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_site_contact_rel',
|
||||
column1='site_id',
|
||||
column2='site_contact_id',
|
||||
domain=[('is_company', '=', False)],
|
||||
tracking=True
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(string='Site Contacts',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_site_contact_rel',
|
||||
column1='site_id',
|
||||
column2='site_contact_id',
|
||||
domain=[('is_company', '=', False)],
|
||||
tracking=True)
|
||||
work_order_contacts = fields.Many2many(
|
||||
string='Work Order Recipients',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_work_order_contacts_rel',
|
||||
column1='res_partner_id',
|
||||
column2='work_order_contact_id',
|
||||
domain=[('is_company', '=', False)],
|
||||
tracking=True
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(string='Work Order Recipients',
|
||||
comodel_name='res.partner',
|
||||
relation='res_partner_work_order_contacts_rel',
|
||||
column1='res_partner_id',
|
||||
column2='work_order_contact_id',
|
||||
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):
|
||||
|
|
@ -50,7 +72,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,28 +1,77 @@
|
|||
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"
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
relation="sale_order_site_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
string='Site Contacts',
|
||||
store=True)
|
||||
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
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
relation='sale_order_work_order_contacts_rel',
|
||||
compute='_compute_default_contacts',
|
||||
inverse='_inverse_default_contacts',
|
||||
string='Work Order Recipients',
|
||||
store=True)
|
||||
summary_equipment_ids = fields.Many2many(
|
||||
comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment Being Serviced",
|
||||
compute="_compute_summary_equipment_ids")
|
||||
|
||||
@api.depends('partner_id')
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name='res.partner',
|
||||
relation="sale_order_site_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
string='Site Contacts',
|
||||
store=True
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name='res.partner',
|
||||
relation='sale_order_work_order_contacts_rel',
|
||||
compute='_compute_default_contacts',
|
||||
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('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,72 +80,21 @@ 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'
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
""" Generate task for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
|
||||
Override to add the logic needed to implement task templates and equipment linkages."""
|
||||
|
||||
def _create_task_from_template(project, template, parent):
|
||||
""" Recursively generates the task and any subtasks from a project.task.template.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
: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)
|
||||
task = self.env['project.task'].sudo().create(values)
|
||||
subtasks = []
|
||||
for t in template.subtasks:
|
||||
subtask = _create_task_from_template(project, t, task)
|
||||
subtasks.append(subtask)
|
||||
task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
|
||||
# We don't want to see the sub-tasks on the SO
|
||||
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):
|
||||
""" 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.
|
||||
Pass the project.project model or an empty recordset to leave task project_id blank.
|
||||
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
|
||||
:param template: project.task.template to use to create the task.
|
||||
: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['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
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
if not tmpl:
|
||||
task = super()._timesheet_create_task(project)
|
||||
else:
|
||||
task = _create_task_from_template(project, tmpl, None)
|
||||
self.write({'task_id': task.id})
|
||||
# post message on task
|
||||
task_msg = _(
|
||||
"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
|
||||
return task
|
||||
|
|
|
|||
267
bemade_fsm/models/sale_order_line.py
Normal file
267
bemade_fsm/models/sale_order_line.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_round
|
||||
import re
|
||||
|
||||
|
||||
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",
|
||||
string="Visits"
|
||||
)
|
||||
|
||||
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.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
|
||||
Override to add the logic needed to implement task templates and equipment linkages."""
|
||||
|
||||
def _create_task_from_template(project, template, parent):
|
||||
""" Recursively generates the task and any subtasks from a project.task.template.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
: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)
|
||||
task = self.env['project.task'].sudo().create(values)
|
||||
subtasks = []
|
||||
for t in template.subtasks:
|
||||
subtask = _create_task_from_template(project, t, task)
|
||||
subtasks.append(subtask)
|
||||
task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
|
||||
# We don't want to see the sub-tasks on the SO
|
||||
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):
|
||||
""" 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.
|
||||
Pass the project.project model or an empty recordset to leave task project_id blank.
|
||||
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
|
||||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
vals = self._timesheet_create_task_prepare_values(project)
|
||||
vals['name'] = template.name
|
||||
vals['description'] = template.description or '' if parent else 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
|
||||
vals['sequence'] = template.sequence
|
||||
if template.equipment_ids:
|
||||
vals['equipment_ids'] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
if not tmpl:
|
||||
task = super()._timesheet_create_task(project)
|
||||
else:
|
||||
task = _create_task_from_template(project, tmpl, None)
|
||||
self.write({'task_id': task.id})
|
||||
# post message on task
|
||||
task_msg = _(
|
||||
"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)
|
||||
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,29 +1,157 @@
|
|||
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",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
compute="_compute_contacts",
|
||||
inverse="_inverse_contacts",
|
||||
store=True)
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_work_order_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
compute="_compute_contacts",
|
||||
inverse="_inverse_contacts",
|
||||
store=True
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(comodel_name="res.partner",
|
||||
relation="task_site_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
compute="_compute_contacts",
|
||||
inverse="_inverse_contacts",
|
||||
store=True)
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_site_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
compute="_compute_contacts",
|
||||
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(
|
||||
string="Start Date",
|
||||
tracking=True,
|
||||
task_dependency_tracking=True,
|
||||
compute="_compute_planned_dates",
|
||||
inverse="_inverse_planned_dates",
|
||||
store=True
|
||||
)
|
||||
|
||||
planned_date_end = fields.Datetime(
|
||||
string="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',
|
||||
string="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 +173,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,25 +1,72 @@
|
|||
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.")
|
||||
parent = fields.Many2one("project.task.template", string="Parent Task Template", ondelete='cascade')
|
||||
subtasks = fields.One2many("project.task.template", inverse_name="parent", string="Subtask Templates")
|
||||
|
||||
assignees = fields.Many2many(
|
||||
comodel_name="res.users",
|
||||
string="Default Assignees",
|
||||
help="Employees assigned to tasks created from this template."
|
||||
)
|
||||
|
||||
customer = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Default Customer",
|
||||
help="Default customer for tasks created from this template."
|
||||
)
|
||||
|
||||
project = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Default Project",
|
||||
help="Default project for tasks created from this template."
|
||||
)
|
||||
|
||||
tags = fields.Many2many(
|
||||
comodel_name="project.tags",
|
||||
string="Default Tags",
|
||||
help="Default tags for tasks created from this template."
|
||||
)
|
||||
|
||||
parent = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Parent Task Template",
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
subtasks = fields.One2many(
|
||||
comodel_name="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)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="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 {
|
||||
'view_mode': 'form',
|
||||
|
|
@ -29,4 +76,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,289 @@
|
|||
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)
|
||||
|
||||
def test_subtask_templates_no_description_if_blank_on_template(self):
|
||||
so = self._generate_sale_order()
|
||||
template = self._generate_task_template(structure=[5], names=['Parent', 'Child'])
|
||||
template.description = ""
|
||||
template.subtasks[0].description = "Some fixed description"
|
||||
for t in template.subtasks[1:]:
|
||||
t.description = ""
|
||||
product = self._generate_product(task_template=template)
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task = sol.task_id
|
||||
self.assertEqual(task.child_ids[0].description, template.subtasks[0].description)
|
||||
for t in task.child_ids[1:]:
|
||||
self.assertFalse(t.description)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@
|
|||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='project_id']" position="after">
|
||||
<field name="task_template_id"
|
||||
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"/>
|
||||
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"
|
||||
domain="[('parent', '=', False)]"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='planning_enabled']" position="after">
|
||||
<field name="is_field_service"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -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,26 @@
|
|||
</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>
|
||||
<record id="view_task_form2_inherit" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="model">project.task</field>
|
||||
<field name="name">bemade_fsm.project_task.form2</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='child_ids']/tree//field[@name='name']" position="after">
|
||||
<field name="description" string="Description/Comments"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
|
||||
|
|
@ -29,48 +55,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_helpdesk_one_ticket_per_email/__init__.py
Normal file
1
bemade_helpdesk_one_ticket_per_email/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
31
bemade_helpdesk_one_ticket_per_email/__manifest__.py
Normal file
31
bemade_helpdesk_one_ticket_per_email/__manifest__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||
#
|
||||
# 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': 'Helpdesk One Ticket Per Email',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Restrict ticket creation to a single ticket per email received.',
|
||||
'category': 'Helpdesk',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['mail'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
1
bemade_helpdesk_one_ticket_per_email/models/__init__.py
Normal file
1
bemade_helpdesk_one_ticket_per_email/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import mail_thread
|
||||
17
bemade_helpdesk_one_ticket_per_email/models/mail_thread.py
Normal file
17
bemade_helpdesk_one_ticket_per_email/models/mail_thread.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
@api.model
|
||||
def _message_route_process(self, message, message_dict, routes):
|
||||
helpdesk_routes = [r for r in routes if r[0] in ('helpdesk.ticket', 'helpdesk.team')]
|
||||
if len(helpdesk_routes) > 1:
|
||||
_logger.info("Messages contained multiple helpdesk routes. Only the first one will be used.")
|
||||
helpdesk_routes.pop(0)
|
||||
routes = routes - helpdesk_routes
|
||||
return super()._message_route_process(message, message_dict, routes)
|
||||
|
|
@ -9,6 +9,8 @@ class MailAlias(models.Model):
|
|||
@api.model
|
||||
def create(self, vals):
|
||||
alias = super(MailAlias, self).create(vals)
|
||||
if not alias.alias_name:
|
||||
return alias
|
||||
|
||||
alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"),
|
||||
catchall_alias = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.alias"),
|
||||
|
|
|
|||
|
|
@ -17,4 +17,9 @@ class ResConfigSettings(models.TransientModel):
|
|||
mailcow_sync_alias = fields.Boolean(
|
||||
string='Sync Aliases with Odoo',
|
||||
help='Auto create Aliases in Mailcow from Odoo',
|
||||
config_parameter='mailcow.sync_alias')
|
||||
config_parameter='mailcow.sync_alias')
|
||||
|
||||
mailcow_auto_create = fields.Boolean(
|
||||
string='Create Mailboxes in Mailcow',
|
||||
help='Auto create Mailboxes in Mailcow on creation in Odoo',
|
||||
config_parameter='mailcow.create_mailbox')
|
||||
|
|
|
|||
|
|
@ -3,13 +3,34 @@ from odoo import models, fields, api
|
|||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
# def _compute_create_mailcow_mailbox(self):
|
||||
# params = self.env['ir.config_parameter'].sudo()
|
||||
# can_create = params.get_param('mailcow.create_mailbox')
|
||||
# return can_create
|
||||
|
||||
mailcow_mailbox = fields.Boolean(string='Mailcow Mailbox', default=False)
|
||||
|
||||
# can_create_mailcow_mailbox = fields.Boolean(
|
||||
# string='Create Mailcow Mailbox',
|
||||
# compute=_compute_create_mailcow_mailbox
|
||||
# )
|
||||
|
||||
mailcow_auto_create = fields.Boolean(compute='_compute_mailcow_auto_create', string='Create Mailbox in Mailcow')
|
||||
|
||||
|
||||
def _compute_mailcow_auto_create(self):
|
||||
for rec in self:
|
||||
config = self.env['ir.config_parameter'].sudo()
|
||||
get_mailcow_auto_create = config.get_param('mailcow_auto_create', False)
|
||||
rec.mailcow_auto_create = get_mailcow_auto_create
|
||||
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(ResUsers, self).create(vals)
|
||||
|
||||
if vals.get('mailcow_mailbox', false):
|
||||
if res.mailcow_mailbox:
|
||||
self.env['mail.mailcow.mailbox'].create_mailbox_for_user(res)
|
||||
|
||||
return res
|
||||
|
||||
|
|
|
|||
|
|
@ -8,29 +8,44 @@
|
|||
<div id="emails" position='after'>
|
||||
<div id="mailcow">
|
||||
<h2>Mailcow Settings</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<label for="mailcow_base_url" string="Mailcow URL"/>
|
||||
<div class="text-muted">
|
||||
Base URL for Mailcow server
|
||||
<div class="row mt16 o_settings_container" name="mailcow_setting">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="mail_setting_creds">
|
||||
<div class="o_setting_right_pane" id="mailcow_settings_creds_settings">
|
||||
<label for="mailcow_base_url" string="Mailcow URL"/>
|
||||
<div class="text-muted">
|
||||
Base URL for Mailcow server
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="mailcow_base_url"/>
|
||||
</div>
|
||||
<label for="mailcow_api_key" string="API Key"/>
|
||||
<div class="text-muted">
|
||||
Mailcow API Key
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="mailcow_api_key"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="mailcow_base_url"/>
|
||||
</div>
|
||||
<label for="mailcow_api_key" string="API Key"/>
|
||||
<div class="text-muted">
|
||||
Mailcow API Key
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="mailcow_api_key"/>
|
||||
</div>
|
||||
<label for="mailcow_sync_alias" string="Sync Aliases with Odoo'"/>
|
||||
<div class="text-muted">
|
||||
Auto create Aliases in Mailcow from Odoo
|
||||
</div>
|
||||
<div class="content-group">
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" name="mailcow_setting_options">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="mailcow_sync_alias"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="mailcow_sync_alias" string="Sync Aliases with Odoo"/>
|
||||
<div class="text-muted">
|
||||
Auto create Aliases in Mailcow from Odoo
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="mailcow_auto_create"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="mailcow_auto_create" string="Create mailbox in Mailcow"/>
|
||||
<div class="text-muted">
|
||||
Auto create Mailbox in Mailcow on creation in Odoo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@
|
|||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Mailcow" name="mailcow">
|
||||
<group>
|
||||
<field name="mailcow_mailbox"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</data>
|
||||
<field name="email" position="before">
|
||||
<div id="mailcow_mailcow" attrs="{'invisible': [('id', '!=', False)]}">
|
||||
<label for="mailcow_mailbox" string="Create mailbox on Mailcow"/>
|
||||
<field name="mailcow_mailbox"/>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
1
bemade_margin_vendor_pricelist/__init__.py
Normal file
1
bemade_margin_vendor_pricelist/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
31
bemade_margin_vendor_pricelist/__manifest__.py
Normal file
31
bemade_margin_vendor_pricelist/__manifest__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 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': 'Sales Margin on Vendor Price',
|
||||
'version': '15.0.0.0.1',
|
||||
'summary': 'Enables calculation of sales margins based on vendor pricelists.',
|
||||
'description': """Adds a new method for calculating """,
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['sale_stock_margin'],
|
||||
'data': ['views/sale_order.xml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * bemade_margin_vendor_pricelist
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-19 15:08+0000\n"
|
||||
"PO-Revision-Date: 2023-10-19 15:08+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_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_actual
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_actual
|
||||
msgid "Margin"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_percent_actual
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_actual
|
||||
msgid "Margin (%)"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_vendor
|
||||
msgid "Margin (%) on Vendor Price"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_vendor
|
||||
msgid "Margin on Vendor Price"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_actual
|
||||
msgid "Purchase Price"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order
|
||||
msgid "Sales Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order_line
|
||||
msgid "Sales Order Line"
|
||||
msgstr ""
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_vendor
|
||||
msgid "Vendor Price"
|
||||
msgstr ""
|
||||
58
bemade_margin_vendor_pricelist/i18n/fr.po
Normal file
58
bemade_margin_vendor_pricelist/i18n/fr.po
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * bemade_margin_vendor_pricelist
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-19 15:08+0000\n"
|
||||
"PO-Revision-Date: 2023-10-19 15:08+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_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_actual
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_actual
|
||||
msgid "Margin"
|
||||
msgstr "Marge"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_percent_actual
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_actual
|
||||
msgid "Margin (%)"
|
||||
msgstr "Marge (%)"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_vendor
|
||||
msgid "Margin (%) on Vendor Price"
|
||||
msgstr "Marge (%) sur le prix du fournisseur"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_vendor
|
||||
msgid "Margin on Vendor Price"
|
||||
msgstr "Marge sur le prix du fournisseur"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_actual
|
||||
msgid "Purchase Price"
|
||||
msgstr "Prix d'achat"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order
|
||||
msgid "Sales Order"
|
||||
msgstr "Commande de vente"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order_line
|
||||
msgid "Sales Order Line"
|
||||
msgstr "Article de commande de vente"
|
||||
|
||||
#. module: bemade_margin_vendor_pricelist
|
||||
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_vendor
|
||||
msgid "Vendor Price"
|
||||
msgstr "Prix du fournisseur"
|
||||
1
bemade_margin_vendor_pricelist/models/__init__.py
Normal file
1
bemade_margin_vendor_pricelist/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import sale_order
|
||||
153
bemade_margin_vendor_pricelist/models/sale_order.py
Normal file
153
bemade_margin_vendor_pricelist/models/sale_order.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.tools.float_utils import float_is_zero, float_compare
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
margin_actual = fields.Monetary("Margin", compute='_compute_margin_actual',
|
||||
store=False)
|
||||
margin_percent_actual = fields.Float('Margin (%)', compute='_compute_margin_actual',
|
||||
store=False,
|
||||
group_operator='avg')
|
||||
|
||||
@api.depends('order_line.margin_actual', 'amount_untaxed')
|
||||
def _compute_margin_actual(self):
|
||||
for order in self:
|
||||
order.margin_actual = sum(order.order_line.mapped('margin_actual'))
|
||||
order.margin_percent_actual = order.amount_untaxed \
|
||||
and order.margin_actual / order.amount_untaxed
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
purchase_price_vendor = fields.Float(compute='_compute_purchase_price_vendor',
|
||||
string="Vendor Price", groups="base.group_user",
|
||||
digits='Product Price')
|
||||
margin_percent_vendor = fields.Float(string='Margin (%) on Vendor Price',
|
||||
groups='base.group_user', group_operator='avg',
|
||||
compute='_compute_margin_vendor')
|
||||
margin_vendor = fields.Float(string='Margin on Vendor Price',
|
||||
groups='base.group_user', digits='Product Price',
|
||||
compute='_compute_margin_vendor')
|
||||
|
||||
purchase_price_actual = fields.Float(compute="_compute_actual_margins",
|
||||
digits='Product Price',
|
||||
groups="base.group_user",
|
||||
string="Purchase Price")
|
||||
margin_actual = fields.Float(compute="_compute_actual_margins",
|
||||
digits='Product Price',
|
||||
groups="base.group_user",
|
||||
string="Margin")
|
||||
margin_percent_actual = fields.Float(compute="_compute_actual_margins",
|
||||
groups="base.group_user",
|
||||
string="Margin (%)")
|
||||
|
||||
@api.depends('purchase_price', 'purchase_price_vendor', 'move_ids.product_id',
|
||||
'move_ids.product_id.qty_available', 'move_ids.state',
|
||||
'qty_to_deliver')
|
||||
def _compute_actual_margins(self):
|
||||
""" We want to use the margin based on average inventory valuation when the
|
||||
sale order line will be completely fulfilled (or has been fulfilled) from stock.
|
||||
For product not yet in stock we want to use the vendor price. We can also have
|
||||
blended calculations (partly on vendor price, partly on stock valuation). This
|
||||
occurs when an order has been or would be partially fulfilled from available
|
||||
stock.
|
||||
:return:
|
||||
"""
|
||||
non_product_lines = self.filtered(lambda r: not r.product_id)
|
||||
non_product_lines.purchase_price_actual = 0.0
|
||||
non_product_lines.margin_actual = 0.0
|
||||
non_product_lines.margin_percent_actual = 0.0
|
||||
for line in self - non_product_lines:
|
||||
stock_missing = line._determine_missing_stock()
|
||||
if float_is_zero(stock_missing,
|
||||
precision_rounding=line.product_uom.rounding):
|
||||
# everything is coming from stock, use inventory valuation
|
||||
line.purchase_price_actual = line.purchase_price
|
||||
elif float_compare(line.product_uom_qty, stock_missing,
|
||||
precision_rounding=line.product_uom.rounding) == 0:
|
||||
# everything is coming from the vendor, use vendor pricing
|
||||
line.purchase_price_actual = line.purchase_price_vendor
|
||||
else:
|
||||
# we have a mix, use blended pricing
|
||||
qty_from_stock = line.product_uom_qty - stock_missing
|
||||
line.purchase_price_actual = \
|
||||
(stock_missing * line.purchase_price_vendor
|
||||
+ qty_from_stock * line.purchase_price) \
|
||||
/ line.product_uom_qty
|
||||
line.margin_actual = line.price_subtotal - (
|
||||
line.purchase_price_actual * line.product_uom_qty)
|
||||
line.margin_percent_actual = line.price_subtotal \
|
||||
and line.margin_actual / line.price_subtotal
|
||||
|
||||
def _determine_missing_stock(self) -> float:
|
||||
""" Compute how much stock is missing to meet an order line's demand. In the
|
||||
case of a quotation line, available stock is checked as if the order were to be
|
||||
placed immediately.
|
||||
|
||||
:return: The quantity missing from available stock to fulfill the line, in
|
||||
the unit of measure matching self.product_uom.
|
||||
"""
|
||||
self.ensure_one()
|
||||
is_order = self.order_id.state in ('sale', 'done')
|
||||
if is_order and self.qty_to_deliver == 0:
|
||||
return 0
|
||||
elif is_order and self.qty_to_deliver > 0:
|
||||
reserved = sum([m.reserved_availability for m in self.move_ids])
|
||||
missing = self.qty_to_deliver - reserved
|
||||
if float_compare(missing, 0.0,
|
||||
precision_rounding=self.product_uom.rounding) == 1:
|
||||
# Not enough reserved, check stock
|
||||
missing = missing - self.product_id.qty_available
|
||||
if float_compare(missing, 0.0,
|
||||
precision_rounding=self.product_uom.rounding) == 1:
|
||||
# Missing some stock to meet demand, return the quantity
|
||||
return missing
|
||||
else:
|
||||
# Enough stock available to meet this line's demand
|
||||
return 0
|
||||
else:
|
||||
# Already have stock reserved
|
||||
return 0
|
||||
else:
|
||||
# This is a quotation, don't bother with stock reservations
|
||||
missing = self.product_uom_qty - self.product_id.qty_available
|
||||
if float_compare(missing, 0.0,
|
||||
precision_rounding=self.product_uom.rounding) == 1:
|
||||
return missing
|
||||
else:
|
||||
return 0
|
||||
|
||||
@api.depends('product_id', 'product_id.seller_ids',
|
||||
'product_id.seller_ids.price')
|
||||
def _compute_purchase_price_vendor(self):
|
||||
for line in self:
|
||||
product = line.product_id
|
||||
suppinfos = product.seller_ids.sorted('sequence')
|
||||
if not suppinfos:
|
||||
line.purchase_price_vendor = 0.0
|
||||
continue
|
||||
suppinfo = suppinfos[0]
|
||||
purch_currency = suppinfo.currency_id
|
||||
to_cur = line.currency_id or line.order_id.currency_id
|
||||
line.purchase_price_vendor = purch_currency._convert(
|
||||
from_amount=suppinfo.price,
|
||||
to_currency=to_cur,
|
||||
company=line.company_id or self.env.company,
|
||||
date=line.order_id.date_order or fields.Date.today(),
|
||||
round=False,
|
||||
) if to_cur and suppinfo.price else suppinfo.price
|
||||
|
||||
@api.depends('purchase_price_vendor')
|
||||
def _compute_margin_vendor(self):
|
||||
for line in self:
|
||||
if not line.price_unit or float_is_zero(line.price_unit):
|
||||
line.margin_vendor = 0
|
||||
line.margin_percent_vendor = 0
|
||||
continue
|
||||
unit_margin = line.price_unit - line.purchase_price_vendor
|
||||
line.margin_percent_vendor = unit_margin / line.price_unit
|
||||
|
||||
line.margin_vendor = unit_margin * line.product_uom_qty
|
||||
51
bemade_margin_vendor_pricelist/views/sale_order.xml
Normal file
51
bemade_margin_vendor_pricelist/views/sale_order.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="sale_margin_sale_order_inherit">
|
||||
<field name="name">sale.order.margin.view.form.inherit</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="margin" position="replace">
|
||||
<field name="margin_actual" class="oe_inline"/>
|
||||
</field>
|
||||
<label for="margin" position="replace">
|
||||
<label for="margin_actual" groups="base.group_user"/>
|
||||
</label>
|
||||
<field name="margin_percent" position="replace">
|
||||
<field name="margin_percent_actual" nolabel="1" class="oe_inline"
|
||||
widget="percentage" groups="base.group_user"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="sale_margin_sale_order_line_inherit">
|
||||
<field name="name">sale.order.line.margin.view.form.inherit</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="purchase_price" position="replace">
|
||||
<field name="purchase_price_actual" groups="base.group_user"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="sale_margin_sale_order_line_form_inherit">
|
||||
<field name="name">sale.order.view.form</field>
|
||||
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line_form"/>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="purchase_price" position="replace">
|
||||
<field name="purchase_price_actual" optional="hide"/>
|
||||
</field>
|
||||
<field name="margin" position="replace">
|
||||
<field name="margin_actual" optional="hide"/>
|
||||
</field>
|
||||
<field name="margin_percent" position="replace">
|
||||
<field name="margin_percent_actual" optional="hide"
|
||||
widget="percentage" groups="base.group_user"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<!-- For now, we don't override the pivot and graph views because our fields
|
||||
are not stored and this will cause performance issues. -->
|
||||
</data>
|
||||
</odoo>
|
||||
12
bemade_module_linker/__init__.py
Normal file
12
bemade_module_linker/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from . import models
|
||||
|
||||
def post_init_hook(cr, registry):
|
||||
# Look for .gitmodules file
|
||||
gitmodules_file = os.path.join(os.getcwd(), '.gitmodules')
|
||||
if os.path.exists(gitmodules_file):
|
||||
# Read .gitmodules file
|
||||
with open(gitmodules_file) as f:
|
||||
content = f.read()
|
||||
# Extract installed directory
|
||||
installed_directory = re.search('[submodule "(.*)"]', content).group(1)
|
||||
print(installed_directory)
|
||||
24
bemade_module_linker/__manifest__.py
Normal file
24
bemade_module_linker/__manifest__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
'name': 'Module Linker',
|
||||
'version': '15.0.1.0.0',
|
||||
'category': 'Extra Tools',
|
||||
'summary': 'Link modules from external repositories',
|
||||
'description': """
|
||||
This module allows users to link modules from external repositories.
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
'base_setup'
|
||||
],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
'author': 'Bemade',
|
||||
'website': 'https://bemade.org/',
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'hooks': [
|
||||
'post_init_hook',
|
||||
],
|
||||
}
|
||||
1
bemade_module_linker/models/__init__.py
Normal file
1
bemade_module_linker/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import res_config_settings
|
||||
8
bemade_module_linker/models/res_config_settings.py
Normal file
8
bemade_module_linker/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
root_repos_directory = fields.Char(string="Root Repos Directory")
|
||||
enabled_addons_directory = fields.Char(string="Enabled Addons Directory")
|
||||
28
bemade_module_linker/models/res_modules_link.py
Normal file
28
bemade_module_linker/models/res_modules_link.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from odoo import fields, models, api
|
||||
from odoo.exceptions import ValidationError
|
||||
import os
|
||||
|
||||
|
||||
class ResModuleLinks(models.Model):
|
||||
_name = 'res.modules.links'
|
||||
_description = 'Module Links'
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
module_id = fields.Many2one('ir.module.module', string='Module')
|
||||
active = fields.Boolean(default=False)
|
||||
repo_name = fields.Char('Repo Name', required=True)
|
||||
|
||||
@api.onchange('active')
|
||||
def on_change_active(self):
|
||||
params = self.env['ir.config_parameter'].sudo()
|
||||
from_dir = params.get_param('root_repos_directory')[0],
|
||||
to_dir = params.get_param('enabled_addons_directory')[0],
|
||||
|
||||
if self.active:
|
||||
os.unlink(os.getcwd() + self.repo_name + '/' + self.name)
|
||||
self.module_id.state = 'installed'
|
||||
else:
|
||||
if self.module_id.state == 'installed':
|
||||
raise ValidationError('You must uninstall the module before deactivating it.')
|
||||
else:
|
||||
os.symlink(os.getcwd() + self.repo_name + '/' + self.name, os.getcwd() + self.repo_name + '/' + self.name)
|
||||
33
bemade_module_linker/views/res_config_settings_views.xml
Normal file
33
bemade_module_linker/views/res_config_settings_views.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_settings_module_linker" model="ir.ui.view">
|
||||
<field name="name">Settings Module Linker</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='about']" position="before">
|
||||
<div id="module_linker">
|
||||
<h2>Module Linker Settings</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<label for="root_repos_directory" string="Root Repos Directory"/>
|
||||
<div class="text-muted">
|
||||
Directory where repos are cloned
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="root_repos_directory"/>
|
||||
</div>
|
||||
<label for="enabled_addons_directory" string="Enabled Addons Directory"/>
|
||||
<div class="text-muted">
|
||||
Enabled Addons Directory
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="enabled_addons_directory"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -31,7 +31,8 @@
|
|||
'account',
|
||||
'bemade_partner_root_ancestor',
|
||||
],
|
||||
'data': [],
|
||||
'data': ['views/account_move_views.xml',
|
||||
'views/res_partner_views.xml'],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class AccountMove(models.Model):
|
|||
string="Billing Contacts",
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts',
|
||||
store=True)
|
||||
store=True,)
|
||||
|
||||
@api.depends('line_ids.sale_line_ids.order_id', 'partner_id')
|
||||
def _compute_billing_contacts(self):
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ class Partner(models.Model):
|
|||
comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts')
|
||||
potential_billing_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts')
|
||||
|
||||
@api.depends('child_ids.type')
|
||||
def _compute_billing_contacts(self):
|
||||
for rec in self:
|
||||
rec.billing_contacts = rec.child_ids.filtered(lambda r: r.type == 'invoice')
|
||||
rec.potential_billing_contacts = rec.child_ids | rec.parent_id.child_ids if rec.is_company else None
|
||||
|
||||
@api.depends('billing_contacts')
|
||||
def _inverse_billing_contacts(self):
|
||||
|
|
|
|||
|
|
@ -34,9 +34,11 @@ class TestBillingContacts(TransactionCase):
|
|||
'parent_id': cls.parent_co.id,
|
||||
'type': 'contact',
|
||||
})
|
||||
cls.product = cls.env['product.product'].with_company(cls.parent_co.company_id).create({
|
||||
cls.product = cls.env['product.product'].with_company(
|
||||
cls.parent_co.company_id).create({
|
||||
'name': 'Product',
|
||||
'categ_id': cls.env['product.category'].create({'name': 'Product Category'}).id,
|
||||
'categ_id': cls.env['product.category'].create(
|
||||
{'name': 'Product Category'}).id,
|
||||
'list_price': 100.0,
|
||||
'type': 'service',
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
|
|
@ -70,19 +72,23 @@ class TestBillingContacts(TransactionCase):
|
|||
self.assertTrue(self.non_billing_contact not in self.parent_co.billing_contacts)
|
||||
|
||||
def test_sale_order_default_billing_contacts(self):
|
||||
self.assertTrue(self.sale_order.billing_contacts == self.parent_co.billing_contacts)
|
||||
self.assertTrue(
|
||||
self.sale_order.billing_contacts == self.parent_co.billing_contacts)
|
||||
|
||||
def test_sale_order_change_contacts(self):
|
||||
# Test that changing the billing contacts on an SO doesn't change them on the partner
|
||||
self.sale_order.write({'billing_contacts': [Command.link(self.non_billing_contact.id)]})
|
||||
self.assertTrue(all([c in self.sale_order.billing_contacts for c in self.parent_co.billing_contacts]))
|
||||
self.sale_order.write(
|
||||
{'billing_contacts': [Command.link(self.non_billing_contact.id)]})
|
||||
self.assertTrue(all([c in self.sale_order.billing_contacts for c in
|
||||
self.parent_co.billing_contacts]))
|
||||
self.assertTrue(self.non_billing_contact not in self.parent_co.billing_contacts)
|
||||
self.assertTrue(self.non_billing_contact in self.sale_order.billing_contacts)
|
||||
|
||||
def test_sale_order_to_invoice_contacts(self):
|
||||
# Test that the invoices created from sales orders take the billing contacts configured on the SO
|
||||
|
||||
self.sale_order.write({'billing_contacts': [Command.link(self.non_billing_contact.id)]})
|
||||
self.sale_order.write(
|
||||
{'billing_contacts': [Command.link(self.non_billing_contact.id)]})
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
wiz = self.env['sale.advance.payment.inv'].create({})
|
||||
|
|
@ -99,10 +105,12 @@ class TestBillingContacts(TransactionCase):
|
|||
self.assertTrue(self.parent_co.billing_contacts == invoice.billing_contacts)
|
||||
|
||||
def test_invoice_followers_on_validate(self):
|
||||
# Make sure all billing contacts get added as followers upon validating the invoice
|
||||
self.sale_order.action_confirm()
|
||||
wiz = self.env['sale.advance.payment.inv'].create({})
|
||||
invoice = wiz._create_invoice(self.sale_order, self.sale_order.order_line[0],
|
||||
self.sale_order.order_line.price_total)
|
||||
invoice.write({'date': datetime.date.today()})
|
||||
invoice.action_post()
|
||||
self.assertTrue(all([r in invoice.message_partner_ids for r in self.parent_co.billing_contacts]))
|
||||
self.assertTrue(all([r in invoice.message_partner_ids for r in
|
||||
self.parent_co.billing_contacts]))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="invoice_view_form" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="name">bemade_multiple_billing_contacts.invoice.view.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="partner_id" position="after">
|
||||
<field name="billing_contacts"
|
||||
attrs="{'invisible': [('move_type', 'not in',
|
||||
('out_invoice', 'out_refund'))]}"
|
||||
widget="many2many_checkboxes"
|
||||
domain="['|', '|', ('id', 'in', billing_contacts),
|
||||
('parent_id', '=', partner_id),
|
||||
('parent_id', 'parent_of', partner_id),
|
||||
('is_company', '=',False)]"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -10,7 +10,9 @@
|
|||
<page name="default_contacts" string="Default Contacts"
|
||||
help="Settings for default contacts to whom different correspondence should be sent.">
|
||||
<group>
|
||||
<field name="billing_contacts" attrs="{'invisible': [('company_type', '=', 'person')]}">
|
||||
<field name="billing_contacts"
|
||||
domain="[('parent_id', '=', id),
|
||||
('is_company', '=', False)]">
|
||||
<tree editable="bottom">
|
||||
<field name="name" widget="res_partner_many2one" />
|
||||
<field name="email" widget="email"/>
|
||||
|
|
|
|||
4
bemade_packing_wizard/__init__.py
Normal file
4
bemade_packing_wizard/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
25
bemade_packing_wizard/__manifest__.py
Normal file
25
bemade_packing_wizard/__manifest__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
'name': 'Packing wizard',
|
||||
'version': '15.0.1.0.0',
|
||||
'category': 'Extra Tools',
|
||||
'summary': 'Allow automated packing type creation',
|
||||
'description': """
|
||||
This module allows users simply enter all the package dimensions on every packing and it will create the packing
|
||||
type automatically if it not exists, use it the create the package with the weight and related package type.
|
||||
|
||||
You need to activate the auto create package feature on the delivery carrier to use this feature.
|
||||
""",
|
||||
'depends': [
|
||||
'delivery'
|
||||
],
|
||||
'data': [
|
||||
'views/stock_package_views.xml',
|
||||
'views/delivery_carrier_views.xml',
|
||||
'wizard/choose_delivery_package_views.xml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
'author': 'Bemade',
|
||||
'website': 'https://bemade.org/',
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
5
bemade_packing_wizard/models/__init__.py
Normal file
5
bemade_packing_wizard/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_quant_package
|
||||
from . import delivery_carrier
|
||||
|
||||
13
bemade_packing_wizard/models/delivery_carrier.py
Normal file
13
bemade_packing_wizard/models/delivery_carrier.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class DeliveryCarrier(models.Model):
|
||||
|
||||
_inherit = 'delivery.carrier'
|
||||
|
||||
# this allow us to enable the auto create package feature per delivery.carrier
|
||||
# default is False to avoid impacting Odoo's default behavior on Test
|
||||
auto_create_package = fields.Boolean('Auto Create Package', default=False)
|
||||
84
bemade_packing_wizard/models/stock_quant_package.py
Normal file
84
bemade_packing_wizard/models/stock_quant_package.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class StockQuantPackage(models.Model):
|
||||
|
||||
_inherit = 'stock.quant.package'
|
||||
|
||||
length = fields.Float('Length')
|
||||
width = fields.Float('Width')
|
||||
height = fields.Float('Height')
|
||||
|
||||
carrier_id = fields.Many2one( comodel_name='delivery.carrier',
|
||||
string='Carrier',
|
||||
help="Carrier use to create package.",
|
||||
compute='_compute_package_carrier' )
|
||||
|
||||
provider = fields.Selection( selection=lambda self: self._get_provider(),
|
||||
string='Provider',
|
||||
help="Provider to create package.",
|
||||
compute='_compute_package_carrier' )
|
||||
|
||||
auto_create_package = fields.Boolean( string='Auto Create Package',
|
||||
compute='_compute_auto_create_package' )
|
||||
|
||||
|
||||
@api.depends('carrier_id')
|
||||
def _compute_auto_create_package(self):
|
||||
# Look so much like a related field
|
||||
for record in self:
|
||||
record.auto_create_package = record.carrier_id.auto_create_package if record.carrier_id else False
|
||||
|
||||
|
||||
def _compute_package_carrier(self):
|
||||
move_line = self.env['stock.move.line'].search([('result_package_id', '=', self.id)])
|
||||
# This also look like a related field and might not even be needed as we only need it to get the provider
|
||||
self.carrier_id = move_line.carrier_id
|
||||
delivery_types_available = [item[0] for item in self.env['stock.package.type']._fields['package_carrier_type'].selection]
|
||||
# This is a bit of a hack to get the provider from the carrier_id, depending if carrier is integrated or not
|
||||
if self.carrier_id.delivery_type not in delivery_types_available:
|
||||
self.provider = False
|
||||
else:
|
||||
self.provider = move_line.carrier_id.delivery_type
|
||||
|
||||
|
||||
|
||||
def _get_provider(self):
|
||||
# just cloning the selection from stock.package.type tru a lambda function
|
||||
return self.env['stock.package.type']._fields['package_carrier_type'].selection
|
||||
|
||||
|
||||
def write(self, vals):
|
||||
if 'length' in vals and 'width' in vals and 'height' in vals:
|
||||
# check if length, width, height are greater than 0
|
||||
if vals['length'] <= 0 or vals['width'] <= 0 or vals['height'] <= 0:
|
||||
raise ValidationError('Length, width, and height must be greater than 0.')
|
||||
# Make sure length is always greater than width, otherwise swap them
|
||||
if vals['length'] < vals['width']:
|
||||
vals['length'], vals['width'] = vals['width'], vals['length']
|
||||
# check if existing package_type exists
|
||||
package_type = self.env['stock.package.type'].search([
|
||||
('width', '=', vals['width']),
|
||||
('height', '=', vals['height']),
|
||||
('packaging_length', '=', vals['length']),
|
||||
('package_carrier_type', '=', self.provider)
|
||||
], limit=1)
|
||||
|
||||
# if not, create new package_type
|
||||
if not package_type:
|
||||
package_type = self.env['stock.package.type'].create({
|
||||
'name': f'Box {vals["length"]}x{vals["width"]}x{vals["height"]}',
|
||||
'width': vals['width'],
|
||||
'height': vals['height'],
|
||||
'packaging_length': vals['length'],
|
||||
'package_carrier_type': self.provider
|
||||
})
|
||||
|
||||
# update package_type_id
|
||||
vals['package_type_id'] = package_type.id
|
||||
|
||||
res = super(StockQuantPackage, self).write(vals)
|
||||
return res
|
||||
13
bemade_packing_wizard/views/delivery_carrier_views.xml
Normal file
13
bemade_packing_wizard/views/delivery_carrier_views.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_delivery_carrier_form_auto_package" model="ir.ui.view">
|
||||
<field name="name">Auto package configuration</field>
|
||||
<field name="model">delivery.carrier</field>
|
||||
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="auto_create_package"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
23
bemade_packing_wizard/views/stock_package_views.xml
Normal file
23
bemade_packing_wizard/views/stock_package_views.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_quant_package_form_size" model="ir.ui.view">
|
||||
<field name="name">stock.view_quant_package_form_size</field>
|
||||
<field name="model">stock.quant.package</field>
|
||||
<field name="inherit_id" ref="stock.view_quant_package_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="package_type_id" position="replace">
|
||||
<field name="auto_create_package" attrs="{'invisible': 1}"/>
|
||||
<field name="package_type_id"
|
||||
attrs="{'invisible': [('package_type_id', '=', False),('auto_create_package', '=', True)]}"/>
|
||||
<field name="carrier_id" attrs="{'invisible': 1}"/>
|
||||
<field name="provider" attrs="{'invisible': 1}"/>
|
||||
<field name="length"
|
||||
attrs="{'invisible': ['|', ('package_type_id', '!=', False), ('auto_create_package', '!=', True)]}"/>
|
||||
<field name="width"
|
||||
attrs="{'invisible': ['|', ('package_type_id', '!=', False), ('auto_create_package', '!=', True)]}"/>
|
||||
<field name="height"
|
||||
attrs="{'invisible': ['|', ('package_type_id', '!=', False), ('auto_create_package', '!=', True)]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
3
bemade_packing_wizard/wizard/__init__.py
Normal file
3
bemade_packing_wizard/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import choose_delivery_package
|
||||
71
bemade_packing_wizard/wizard/choose_delivery_package.py
Normal file
71
bemade_packing_wizard/wizard/choose_delivery_package.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from math import ceil
|
||||
from odoo.exceptions import ValidationError
|
||||
class ChooseDeliveryPackage(models.TransientModel):
|
||||
_inherit = 'choose.delivery.package'
|
||||
|
||||
length = fields.Float('Length')
|
||||
width = fields.Float('Width')
|
||||
height = fields.Float('Height')
|
||||
auto_create_package = fields.Boolean(string='Auto Create Package', default=False)
|
||||
provider = fields.Selection(selection=lambda self: self._get_provider(), string='Provider')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
# This is a bit of a hack to get the provider from the carrier_id, depending if carrier is integrated or not
|
||||
defaults = super().default_get(fields_list)
|
||||
if 'provider' in fields_list:
|
||||
# get related picking
|
||||
picking = self.env['stock.picking'].browse(defaults.get('picking_id'))
|
||||
# generate list of available delivery types keeping only the first element of the tuple
|
||||
delivery_types_available = \
|
||||
[item[0] for item in self.env['stock.package.type']._fields['package_carrier_type'].selection]
|
||||
# if carrier is not integrated, provider is False
|
||||
if picking.carrier_id.delivery_type not in delivery_types_available:
|
||||
defaults['provider'] = False
|
||||
defaults['auto_create_package'] = False
|
||||
else:
|
||||
# if carrier is integrated, provider is the delivery_type and carry auto_create_package from carrier
|
||||
defaults['provider'] = picking.carrier_id.delivery_type
|
||||
defaults['auto_create_package'] = picking.carrier_id.auto_create_package
|
||||
return defaults
|
||||
|
||||
|
||||
def _get_provider(self):
|
||||
# just cloning the selection from stock.package.type tru a lambda function
|
||||
return self.env['stock.package.type']._fields['package_carrier_type'].selection
|
||||
|
||||
|
||||
def action_put_in_pack(self):
|
||||
# this override is call only for delivery.carrier with auto_create_package = True. Ideal for odoo test
|
||||
if self.auto_create_package:
|
||||
if self.width <= 0 or self.height <= 0 or self.length <= 0:
|
||||
# Obviously, we need value for our wizard to work
|
||||
raise ValidationError('Length, width, and height must be greater than 0.')
|
||||
|
||||
# Make sure length is always greater than width, otherwise swap them
|
||||
if self.length < self.width:
|
||||
self.length, self.width = self.width, self.length
|
||||
|
||||
# that search is carier.delivery integration agnostic
|
||||
delivery_package_type = self.env['stock.package.type'].search([
|
||||
('width', '=', self.width),
|
||||
('height', '=', self.height),
|
||||
('packaging_length', '=', self.length),
|
||||
('package_carrier_type', '=', self.provider)
|
||||
], limit=1)
|
||||
|
||||
# if no package type found, create one
|
||||
if not delivery_package_type:
|
||||
delivery_package_type = delivery_package_type.create({
|
||||
'name': f'Box {vals["width"]}x{vals["height"]}x{vals["length"]}',
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'packaging_length': self.length,
|
||||
'package_carrier_type': self.provider
|
||||
})
|
||||
|
||||
self.delivery_package_type_id = delivery_package_type.id
|
||||
result = super().action_put_in_pack()
|
||||
return result
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="choose_delivery_package_view_form2" model="ir.ui.view">
|
||||
<field name="name">choose.delivery.package.form2</field>
|
||||
<field name="model">choose.delivery.package</field>
|
||||
<field name="inherit_id" ref="delivery.choose_delivery_package_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="delivery_package_type_id" position="replace">
|
||||
<field name="auto_create_package" attrs="{'invisible': 1}"/>
|
||||
<field name="delivery_package_type_id"
|
||||
domain="[('package_carrier_type', '=', context.get('current_package_carrier_type', 'none'))]"
|
||||
context="{'form_view_ref':'stock.stock_package_type_form'}"
|
||||
attrs="{'invisible': [('auto_create_package', '=', True)]}" />
|
||||
<field name="length" attrs="{'invisible': [('auto_create_package', '=', False)]}"/>
|
||||
<field name="width" attrs="{'invisible': [('auto_create_package', '=', False)]}"/>
|
||||
<field name="height" attrs="{'invisible': [('auto_create_package', '=', False)]}"/>
|
||||
<field name="provider" attrs="{'invisible': 1}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
2
bemade_partner_email_domain/__init__.py
Normal file
2
bemade_partner_email_domain/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
38
bemade_partner_email_domain/__manifest__.py
Normal file
38
bemade_partner_email_domain/__manifest__.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
'name': 'Automated Partner Association by Email Domain',
|
||||
'version': '15.0.0.0.0',
|
||||
'category': 'Extra Tools',
|
||||
'summary': 'Automatically associates partners with companies using matching email domains',
|
||||
'description': """
|
||||
This advanced utility module empowers Odoo users with the capacity to automate partner-company associations
|
||||
based directly on email domains. It helps in eliminating manual effort by establishing a rule of correlation
|
||||
between the email domain of a partner and the corresponding division or a company.
|
||||
|
||||
The automation proceeds as follows:
|
||||
|
||||
- On creation of partner records, the system will cross-verify the email domain against the companies
|
||||
in the records.
|
||||
- If a match is detected, the partner will automatically be linked with that company.
|
||||
|
||||
This module also takes care of instances where there are multiple companies with the same email domain.
|
||||
In such cases, an email is dispatched to the partner with a selection interface to finalize the association.
|
||||
|
||||
The module is a significant productivity enhancer for businesses that deal with a high number of partners
|
||||
and require efficient management.
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
'mail',
|
||||
'website'
|
||||
],
|
||||
'data': [
|
||||
'views/res_partner_views.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/template_data.xml'
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
'author': 'Bemade',
|
||||
'website': 'https://bemade.org/',
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
1
bemade_partner_email_domain/controllers/__init__.py
Normal file
1
bemade_partner_email_domain/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
40
bemade_partner_email_domain/controllers/main.py
Normal file
40
bemade_partner_email_domain/controllers/main.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Import necessary libraries
|
||||
from odoo import http, _
|
||||
#from odoo.http import request
|
||||
|
||||
|
||||
# Define a controller class that inherits from http.Controller
|
||||
class DivisionCompanyController(http.Controller):
|
||||
|
||||
# Decorator to set the routing rules for the method 'select_division_company'.
|
||||
# The URL for this method will be '/select_division_company'
|
||||
# It has public access and should be used for HTTP requests of type GET.
|
||||
@http.route('/select_division_company', website=True, type='http', auth='public')
|
||||
def select_division_company(self, partner_id, access_token, division_id, **kwargs):
|
||||
|
||||
# Search the 'res.partner' model for a record with the given partner_id using Super User access rights
|
||||
partner = http.request.env['res.partner'].sudo().search([
|
||||
('id', '=', int(partner_id))
|
||||
])
|
||||
|
||||
# Check if a partner was found with the given ID
|
||||
if not partner:
|
||||
# If no partner found, render an error page with a translatable error message
|
||||
return http.request.render('bemade_partner_email_domain.error_page',
|
||||
{'error_message': _('The requested partner is not found.')})
|
||||
else:
|
||||
# If the partner exists and the access token matches, proceed with updating the partner's division
|
||||
if access_token and access_token == partner.access_token:
|
||||
# Update the partner's parent with the division_id
|
||||
partner.write({'parent_id': int(division_id)})
|
||||
|
||||
# Set a confirmation message signaling that the partner has been associated with the division
|
||||
confirmation_message = _('Partner %s is associated with the division %s.' % (partner.name, division_id))
|
||||
|
||||
# Render the error page (which in this case acts as a status page) with the confirmation message.
|
||||
return http.request.render('bemade_partner_email_domain.error_page',
|
||||
{'confirmation_message': confirmation_message})
|
||||
else:
|
||||
# If the access token was not provided or does not match, render an error page with an error message
|
||||
return http.request.render('bemade_partner_email_domain.error_page',
|
||||
{'error_message': _('No Access Token found or not matching for this partner.')})
|
||||
107
bemade_partner_email_domain/data/mail_template_data.xml
Normal file
107
bemade_partner_email_domain/data/mail_template_data.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="email_template_select_parent" model="mail.template">
|
||||
<field name="name">Division Selection: Choose Your Division</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="email_from">{{object.company_id.email_formatted or user.email_formatted}}</field>
|
||||
<field name="partner_to">{{object.id}}</field>
|
||||
<field name="subject">Select Your Division at {{object.company_id.name}}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial, sans-serif; color: #454748; width: 100%; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<!-- Content here -->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top" style="font-size: 13px;">
|
||||
<p>Hello <t t-out="object.name or ''">Brandon Freeman</t>,</p>
|
||||
<p>We noticed that you have registered with our company by sending an email or using our website. To provide you with the best possible service, we need you to confirm the division of your company that you are working for.</p>
|
||||
<p>Please click on the link below to select your division:</p>
|
||||
<ul>
|
||||
<t t-foreach="ctx.get('links', {}).items()" t-as="company_name">
|
||||
<li>
|
||||
<a t-attf-href="{{ company_name[1] }}" style="font-size:12px">
|
||||
<t t-esc="company_name[0]"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<p>If you have any questions or did not register with us, please disregard this email or contact us directly.</p>
|
||||
<p>Best regards,</p>
|
||||
<p>The Durpro team.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- More content can be added here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="middle" align="left">
|
||||
<t t-out="object.company_id.name or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="middle" align="left" style="opacity: 0.7;">
|
||||
<t t-out="object.company_id.phone or ''"/>
|
||||
<t t-if="object.company_id.email">
|
||||
| <a t-att-href="'mailto:%s' % object.company_id.email" style="text-decoration:none; color: #454748;" t-out="object.company_id.email or ''"></a>
|
||||
</t>
|
||||
<t t-if="object.company_id.website">
|
||||
| <a t-att-href="'%s' % object.company_id.website" style="text-decoration:none; color: #454748;" t-out="object.company_id.website or ''"></a>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- POWERED BY -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: center; font-size: 13px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=auth" style="color: #875A7B;">Odoo</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</field>
|
||||
<field name="lang">{{ object.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
17
bemade_partner_email_domain/data/template_data.xml
Normal file
17
bemade_partner_email_domain/data/template_data.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<odoo>
|
||||
<data>
|
||||
<template id="error_page">
|
||||
<base href="/" />
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<t t-if="error_message">
|
||||
<div class="alert alert-danger"><t t-esc="error_message"/></div>
|
||||
</t>
|
||||
<t t-if="confirmation_message">
|
||||
<div class="alert alert-success"><t t-esc="confirmation_message"/></div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
41
bemade_partner_email_domain/i18n/fr_CA.po
Normal file
41
bemade_partner_email_domain/i18n/fr_CA.po
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * bemade_partner_email_domain
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0\n"
|
||||
"Report-Msgid-Bugs-To: it@bemade.org\n"
|
||||
"Last-Translator: chatgpt\n"
|
||||
"Language-Team: French (http://www.transifex.com/odoo/odoo-14-0/language/fr_CA/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: model:ir.model.fields,field_description:bemade_partner_email_domain.field_res_partner_access_token
|
||||
msgid "Access Token"
|
||||
msgstr "Jeton d'accès"
|
||||
|
||||
#: model:ir.model.fields,field_description:bemade_partner_email_domain.field_res_partner_email_domain
|
||||
msgid "Email Domain"
|
||||
msgstr "Domaine de courriel"
|
||||
|
||||
#: model:ir.model.fields,field_description:bemade_partner_email_domain.field_res_partner_is_subdivision
|
||||
msgid "Subdivision"
|
||||
msgstr "Subdivision"
|
||||
|
||||
#: code:addons/bemade_partner_email_domain/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "The requested partner is not found."
|
||||
msgstr "Le partenaire demandé n'est pas trouvé."
|
||||
|
||||
#: code:addons/bemade_partner_email_domain/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "No Access Token found or not matching for this partner."
|
||||
msgstr "Aucun jeton d'accès trouvé ou ne correspondant pas à ce partenaire."
|
||||
|
||||
#: code:addons/bemade_partner_email_domain/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Partner %s is associated with the division %s."
|
||||
msgstr "Le partenaire %s est associé à la division %s."
|
||||
1
bemade_partner_email_domain/models/__init__.py
Normal file
1
bemade_partner_email_domain/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import res_partner
|
||||
74
bemade_partner_email_domain/models/res_partner.py
Normal file
74
bemade_partner_email_domain/models/res_partner.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from odoo import api, fields, models, Command
|
||||
import uuid
|
||||
|
||||
from odoo.addons.website.controllers.main import Website
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
email_domain = fields.Char(string='Email Domain')
|
||||
is_subdivision = fields.Boolean(string='Subdivision', default=False)
|
||||
access_token = fields.Char(string='Access Token')
|
||||
|
||||
def _generate_access_token(self):
|
||||
return uuid.uuid4().hex
|
||||
|
||||
def _send_selection_email(self, division_companies):
|
||||
self.ensure_one()
|
||||
# Generate a token and save it to the partner
|
||||
access_token = self._generate_access_token()
|
||||
print(f'partner id: {self.id}')
|
||||
self.write({'access_token': access_token})
|
||||
|
||||
# Now include the token in the links
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
links = {
|
||||
company.name: (f'{base_url}/select_division_company?partner_id={self.id}&access_token={access_token}&division_id={company.id}') for company in division_companies
|
||||
}
|
||||
|
||||
print(f'links: {links}')
|
||||
# Render the email template and send the email
|
||||
template = self.env.ref('bemade_partner_email_domain.email_template_select_parent')
|
||||
template.with_context(links=links).send_mail(self.id, force_send=True)
|
||||
|
||||
# @api.onchange('email')
|
||||
def _check_parent_from_email_domain(self):
|
||||
for rec in self:
|
||||
if rec.parent_id:
|
||||
continue
|
||||
try:
|
||||
# Split the email address on '@' and get the domain part
|
||||
if rec.email:
|
||||
email_domain = rec.email.split('@')[1].strip()
|
||||
else:
|
||||
continue
|
||||
except IndexError:
|
||||
# If there's no '@' symbol, the email address is invalid
|
||||
return False
|
||||
|
||||
# Loop while there's more than one part in the domain (e.g., subdomain.domain.tld)
|
||||
while '.' in email_domain:
|
||||
# If the current email domain matches the main domain, return True
|
||||
company_domain = self.env['res.partner'].search([('email_domain', 'ilike', email_domain)])
|
||||
if company_domain:
|
||||
if len(company_domain) > 1:
|
||||
rec._send_selection_email(company_domain)
|
||||
else:
|
||||
rec.parent_id = company_domain.id
|
||||
return
|
||||
|
||||
# If not, drop the part before the first '.' to check the next level
|
||||
email_domain = email_domain.split('.', 1)[1]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super(Partner, self).create(vals_list)
|
||||
res._check_parent_from_email_domain()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super(Partner, self).write(vals)
|
||||
if 'email' in vals:
|
||||
self._check_parent_from_email_domain()
|
||||
return res
|
||||
21
bemade_partner_email_domain/views/res_partner_views.xml
Normal file
21
bemade_partner_email_domain/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="partner_email_domain_form" model="ir.ui.view">
|
||||
<field name="name">bemade_partner_email_domain.partner.email.domain.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_type" position="after">
|
||||
<label for="is_subdivision" string="Division" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
<field name="is_subdivision" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
</field>
|
||||
<field name="website" position="before">
|
||||
<field name="email_domain" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
</field>
|
||||
<field name="parent_id" position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('is_company','=', True),('is_subdivision','=', False)]}
|
||||
</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue