Merge branch 'development' into 'master'

Development

See merge request prakash.jain/cor-odoo!47
This commit is contained in:
prakash.jain 2021-01-03 22:15:03 -08:00
commit 46bc5f2da0
413 changed files with 105825 additions and 6 deletions

3
.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
# compiled python files
*.py[co]
__pycache__/

0
README.md Normal file → Executable file
View File

3
board_user/__init__.py Executable file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

32
board_user/__manifest__.py Executable file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
{
'name': "Dashboard per user",
'summary': """
This module helps you to see other's user dashboard""",
'description': """
Long description of module's purpose
""",
'license': 'AGPL-3',
'author': "Apanda",
'support': "ryantsoa@gmail.com",
'category': 'Uncategorized',
'version': '1.0',
# any module necessary for this one to work correctly
'depends': ['base', 'board'],
# always loaded
'data': [
'security/board_users.xml',
'security/ir.model.access.csv',
'views/board_users_view.xml',
'views/assets_backend.xml',
],
'qweb': ['static/src/xml/board.xml'],
'images': ['static/description/cover.gif'],
}

3
board_user/models/__init__.py Executable file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import board_users

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from odoo import tools
from odoo import models, fields, api
class board_user(models.Model):
_name = 'board.users'
_auto = False
user_id = fields.Many2one('res.users', string='User', readonly=True)
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
create or replace view board_users as (
select
min(i.id) as id,
i.user_id as user_id
from
ir_ui_view_custom as i
group by
i.user_id
)
""")
class Board(models.AbstractModel):
_inherit = 'board.board'
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
user_dashboard = self.env.context.get('user_dashboard')
if user_dashboard:
self = self.with_env(self.env(user=user_dashboard))
res = super(Board, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
return res

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="all_dashboards" model="res.groups">
<field name="name">All dashboards </field>
<field name="comment">the user will have an access to the the menu all dashboards</field>
<field name="category_id" ref="base.module_category_usability"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_board_user_board_user,board_user.board_user,model_board_users,board_user.all_dashboards,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_board_user_board_user board_user.board_user model_board_users board_user.all_dashboards 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,25 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">See all dashboards of each user</h2>
<h3 class="oe_slogan">Get the list of users who sets up a dashboard.</h3>
<div class="oe_demo oe_picture oe_screenshot">
<img src="all_dashboards.gif">
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Settings</h2>
<div class="oe_span6 text-justify oe_mt32">
<span class="fa fa-cog fa-2x pull-left"/>
<p class="oe_mb32" style="margin-left:48px;">
Set which users can see others dashboards.
</p>
</div>
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="settings.gif">
</div>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,46 @@
odoo.define('web.AbstractControllerBoardUser', function (require) {
"use strict";
var AbstractController = require('web.AbstractController');
AbstractController.include({
_onOpenRecord: function (event) {
event.stopPropagation();
var record = this.model.get(event.data.id, {raw: true});
var self = this;
var res_id = record.res_id;
var model = this.modelName;
if (model == "board.users"){
self._rpc({
model: 'ir.model.data',
method: 'xmlid_to_res_model_res_id',
args:["board.board_my_dash_view"],
})
.then(function (data) {
self.do_action({
name: "Dashboard",
type: "ir.actions.act_window",
res_model: "board.board",
views: [[data[1], 'form']],
usage: 'menu',
context: {'user_dashboard': record.data.user_id},
view_type: 'form',
view_mode: 'form',
});
});
}
else{
self.trigger_up('switch_view', {
view_type: 'form',
res_id: res_id,
mode: event.data.mode || 'readonly',
model: model
});
}
},
});
});

View File

@ -0,0 +1,78 @@
odoo.define('web.BoardBoardUser', function (require) {
"use strict";
var BoardView = require('board.BoardView');
var core = require('web.core');
var QWeb = core.qweb;
var Domain = require('web.Domain');
BoardView.prototype.config.Renderer.include({
_renderTagBoard: function (node) {
var self = this;
// we add the o_dashboard class to the renderer's $el. This means that
// this function has a side effect. This is ok because we assume that
// once we have a '<board>' tag, we are in a special dashboard mode.
this.$el.addClass('o_dashboard');
this.trigger_up('enable_dashboard');
var hasAction = _.detect(node.children, function (column) {
return _.detect(column.children,function (element){
return element.tag === "action"? element: false;
});
});
if (!hasAction) {
return $(QWeb.render('DashBoard.NoContent'));
}
// We should start with three columns available
node = $.extend(true, {}, node);
// no idea why master works without this, but whatever
if (!('layout' in node.attrs)) {
node.attrs.layout = node.attrs.style;
}
for (var i = node.children.length; i < 3; i++) {
node.children.push({
tag: 'column',
attrs: {},
children: []
});
}
// register actions, alongside a generated unique ID
_.each(node.children, function (column, column_index) {
_.each(column.children, function (action, action_index) {
action.attrs.id = 'action_' + column_index + '_' + action_index;
self.actionsDescr[action.attrs.id] = action.attrs;
});
});
var state_context = self.state.context;
if (state_context.hasOwnProperty('user_dashboard')){
node.perm_close = self.state.context.uid === self.state.context.user_dashboard;
}
else{
node.perm_close = true;
}
var $html = $('<div>').append($(QWeb.render('DashBoard', {node: node})));
// render each view
_.each(this.actionsDescr, function (action) {
self.defs.push(self._createController({
$node: $html.find('.oe_action[data-id=' + action.id + '] .oe_content'),
actionID: _.str.toNumber(action.name),
context: action.context,
domain: Domain.prototype.stringToArray(action.domain, {}),
viewType: action.view_mode,
}));
});
$html.find('.oe_dashboard_column').sortable({
connectWith: '.oe_dashboard_column',
handle: '.oe_header',
scroll: false
}).bind('sortstop', function () {
self.trigger_up('save_dashboard');
});
return $html;
},
});
});

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<template>
<t t-extend="DashBoard">
<t t-jquery=".oe_dashboard_links" t-operation="attributes">
<attribute name="t-if">node.perm_close</attribute>
</t>
<t t-jquery="t[t-call='DashBoard.action']" t-operation="replace">
<t t-if="node.perm_close">
<t t-foreach="column.children" t-as="action" t-if="action.tag == 'action'" t-call="DashBoard.action"/>
</t>
<t t-else="">
<t t-foreach="column.children" t-as="action" t-if="action.tag == 'action'" t-call="DashBoard.action.no_close"/>
</t>
</t>
</t>
<t t-name="DashBoard.action.no_close">
<div t-att-data-id="action.attrs.id" class="oe_action">
<h2 t-attf-class="oe_header #{action.attrs.string ? '' : 'oe_header_empty'}">
<span class="oe_header_txt"> <t t-esc="action.attrs.string"/> </span>
<input class = "oe_header_text" type="text"/>
<t t-if="!action.attrs.string">&amp;nbsp;</t>
</h2>
<div class="oe_content"/>
</div>
</t>
</template>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="board_users_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/board_user/static/src/js/abstract_controller_inherit.js"></script>
<script type="text/javascript" src="/board_user/static/src/js/board_view_inherit.js"></script>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,28 @@
<odoo>
<data>
<!-- explicit list view definition -->
<record model="ir.ui.view" id="board_user_list">
<field name="name">board_user list</field>
<field name="model">board.users</field>
<field name="arch" type="xml">
<tree edit="false">
<field name="user_id"/>
</tree>
</field>
</record>
<!-- actions opening views on models -->
<record model="ir.actions.act_window" id="board_user_action">
<field name="name">All dashboards per user</field>
<field name="res_model">board.users</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="All dashboards" id="board_user_menu" parent="base.menu_board_root"
action="board_user_action"/>
</data>
</odoo>

6
cor_custom/__init__.py Executable file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import wizard
#from . import report

45
cor_custom/__manifest__.py Executable file
View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
{
'name': "cor_custom",
'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'crm', 'sale_timesheet', 'analytic', 'hr'],
# always loaded
'data': [
#'security/ir.model.access.csv',
#'security/cor_custom_security.xml',
'views/crm_view.xml',
'views/sale_views.xml',
'views/project_view.xml',
'views/hr_employee_views.xml',
'views/hr_timesheet_templates.xml',
'views/analytic_view.xml',
'report/project_profitability_report_analysis_views.xml',
'views/views.xml',
'views/templates.xml',
#'views/menu_show_view.xml',
'wizard/project_create_sale_order_views.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# from odoo import http
# class CorCustom(http.Controller):
# @http.route('/cor_custom/cor_custom/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/cor_custom/cor_custom/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('cor_custom.listing', {
# 'root': '/cor_custom/cor_custom',
# 'objects': http.request.env['cor_custom.cor_custom'].search([]),
# })
# @http.route('/cor_custom/cor_custom/objects/<model("cor_custom.cor_custom"):obj>/', auth='public')
# def object(self, obj, **kw):
# return http.request.render('cor_custom.object', {
# 'object': obj
# })

30
cor_custom/demo/demo.xml Executable file
View File

@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="cor_custom.cor_custom">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="cor_custom.cor_custom">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="cor_custom.cor_custom">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="cor_custom.cor_custom">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="cor_custom.cor_custom">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>

510
cor_custom/i18n/he_IL.po Executable file
View File

@ -0,0 +1,510 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cor_custom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-12-28 06:13+0000\n"
"PO-Revision-Date: 2020-12-28 06:13+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: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_timesheet_plan
msgid "<b>Total</b>"
msgstr "סך הכל"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__act_project_manager_id
msgid "Actual Project Manager"
msgstr "מנהל פרויקטים בפועל"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__admin_user
msgid "Admin User"
msgstr "משתמש מנהל"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__privacy_visibility__employees
msgid "All internal users"
msgstr "כל המשתמשים הפנימיים"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_account_analytic_line
msgid "Analytic Line"
msgstr "שורה אנליטית"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner__lead_id
msgid "Associated Lead"
msgstr "ליד משויך"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__budgeted_hour_week
msgid "Budgeted Hours per week"
msgstr "שעות מתוקצבות לשבוע"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__budgeted_qty
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__budgeted_qty
msgid "Budgeted Qty"
msgstr "כמות מתוקצבת"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__budgeted_uom
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__budgeted_uom
msgid "Budgeted UOM"
msgstr "UOM מתוקצב"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__partner_id
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner__partner_id
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order__partner_id
#: model:ir.model.fields,field_description:cor_custom.field_sale_order__partner_id
#: model_terms:ir.ui.view,arch_db:cor_custom.crm_lead_form_cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_view_task_form2
#: model_terms:ir.ui.view,arch_db:cor_custom.project_project_analytic_view_form
msgid "Client"
msgstr "לָקוּחַ"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__client_folder
msgid "Client Folder"
msgstr "תיקיית לקוח"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_product_product__taxes_id
#: model:ir.model.fields,field_description:cor_custom.field_product_template__taxes_id
msgid "Client Taxes"
msgstr "מיסי לקוח"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_project__bill_type
msgid "Client Type"
msgstr "סוג לקוח"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_create_sale_order__partner_id
msgid "Client of the sales order"
msgstr "לקוח של הזמנת המכירה"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__close_date
msgid "Close Date"
msgstr "תאריך קרוב"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__employee_id
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_project_project_view_form
msgid "Consultant"
msgstr "יוֹעֵץ"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__employee_price
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__employee_price
msgid "Consultant Price"
msgstr "מחיר יועץ"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__pricing_type__employee_rate
msgid "Consultant rate"
msgstr "תעריף יועץ"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_create_sale_order_line__employee_id
msgid "Consultant that has timesheets on the project."
msgstr "יועץ שיש לו לוחות זמנים על הפרויקט"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__contract
msgid "Contract"
msgstr "חוֹזֶה"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.project_project_analytic_view_form
msgid "Cost/Revenue"
msgstr "עלות / הכנסה"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_project_create_sale_order_line
msgid "Create SO Line from project"
msgstr "צור שורת הזמנת לקוח מפרויקט"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_project_create_sale_order
msgid "Create SO from project"
msgstr "צור הזמנת לקוח מפרויקט"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__crm_quotation_partner__action__create
msgid "Create a new client"
msgstr "צור לקוח חדש"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_crm_quotation_partner
msgid "Create new or use existing Customer on new Quotation"
msgstr "צור לקוח חדש או השתמש בקיים בהצעת מחיר חדשה"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_project_project_view_form
msgid "Default Sales Order Item"
msgstr "פריט הזמנת מכר המוגדר כברירת מחדל"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_project_project_view_form
msgid "Default Service"
msgstr "שירות ברירת מחדל"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_product_product__taxes_id
#: model:ir.model.fields,help:cor_custom.field_product_template__taxes_id
msgid "Default taxes used when selling the product."
msgstr "מיסים ברירת מחדל המשמשים בעת מכירת המוצר."
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_create_sale_order_line__budgeted_uom
msgid "Default unit of measure used for all stock operations."
msgstr "חידת מידה המוגדרת כברירת מחדל המשמשת לכל פעולות המניות"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_project__privacy_visibility
msgid ""
"Defines the visibility of the tasks of the project:\n"
"- Invited internal users: employees may only see the followed project and tasks.\n"
"- All internal users: employees may see all project and tasks.\n"
"- Invited portal and all internal users: employees may see everything. Portal users may see project and tasks followed by\n"
" them or by someone of their company."
msgstr ""
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_account_analytic_line__display_name
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__display_name
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner__display_name
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee__display_name
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee_public__display_name
#: model:ir.model.fields,field_description:cor_custom.field_product_product__display_name
#: model:ir.model.fields,field_description:cor_custom.field_product_template__display_name
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order__display_name
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__display_name
#: model:ir.model.fields,field_description:cor_custom.field_project_project__display_name
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__display_name
#: model:ir.model.fields,field_description:cor_custom.field_sale_order__display_name
msgid "Display Name"
msgstr "הצג שם"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__crm_quotation_partner__action__nothing
msgid "Do not link to a client"
msgstr "אל תקשר לקוח"
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_crm_lead_email_from_uniq
msgid "Email ID already exists !"
msgstr "Email ID already exists !"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_hr_employee
msgid "Employee"
msgstr "עובד"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_account_analytic_line__end_time
msgid "End Time"
msgstr "זמן סיום"
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_account_analytic_line_check_end_time_positive
msgid "End hour must be a positive number"
msgstr "שעת סיום חייבת להיות מספר חיובי"
#. module: cor_custom
#: code:addons/cor_custom/models/analytic.py:0
#: code:addons/cor_custom/models/analytic.py:0
#, python-format
msgid "End time cannot be earlier than Start time"
msgstr "זמן הסיום לא יכול להיות מוקדם יותר משעת ההתחלה"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__pricing_type__fixed_rate
msgid "Fixed rate"
msgstr "שער קבוע"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_timesheet_plan
msgid "Hourly Rate"
msgstr "תעריף לשעה"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__project_type__hours_in_consultant
msgid "Hours are budgeted according to a consultant"
msgstr "השעות מתוקצבות על פי יועץ"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_account_analytic_line__id
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__id
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner__id
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee__id
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee_public__id
#: model:ir.model.fields,field_description:cor_custom.field_product_product__id
#: model:ir.model.fields,field_description:cor_custom.field_product_template__id
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order__id
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line__id
#: model:ir.model.fields,field_description:cor_custom.field_project_project__id
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__id
#: model:ir.model.fields,field_description:cor_custom.field_sale_order__id
msgid "ID"
msgstr "תעודה מזהה"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__privacy_visibility__followers
msgid "Invited internal users"
msgstr "משתמשים פנימיים מוזמנים"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__privacy_visibility__portal
msgid "Invited portal users and all internal users"
msgstr "משתמשי פורטל מוזמנים וכל המשתמשים הפנימיים"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__bill_type__customer_project
msgid "Invoice all tasks to a single client"
msgstr "חשב את כל המשימות ללקוח יחיד"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__bill_type__customer_task
msgid "Invoice tasks separately to different clients"
msgstr "משימות חשבונית בנפרד ללקוחות שונים"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_project_project_view_form
msgid "Invoicing"
msgstr "חשבונית"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_account_analytic_line____last_update
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead____last_update
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner____last_update
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee____last_update
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee_public____last_update
#: model:ir.model.fields,field_description:cor_custom.field_product_product____last_update
#: model:ir.model.fields,field_description:cor_custom.field_product_template____last_update
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order____last_update
#: model:ir.model.fields,field_description:cor_custom.field_project_create_sale_order_line____last_update
#: model:ir.model.fields,field_description:cor_custom.field_project_project____last_update
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map____last_update
#: model:ir.model.fields,field_description:cor_custom.field_sale_order____last_update
msgid "Last Modified on"
msgstr "שינוי אחרון ב"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__lead_no
msgid "Lead ID"
msgstr "תעודת זהות"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_crm_lead
msgid "Lead/Opportunity"
msgstr "ליד/הזדמנות"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__crm_quotation_partner__action__exist
msgid "Link to an existing client"
msgstr "קישור ללקוח קיים"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_crm_lead__partner_id
msgid ""
"Linked partner (optional). Usually created when converting the lead. You can"
" find a partner by its Name, TIN, Email or Internal Reference."
msgstr ""
"לקוח/ספק מקושר (אופציונלי). נוצר בדרך כלל בעת המרת הליד. אתה יכול למצוא לקוח"
" לפי שם, דוא\"ל או מזהה פנימי."
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__crm_lead__contract__no
msgid "No"
msgstr "לא"
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_crm_lead_phone_uniq
msgid "Phone No already exists !"
msgstr "טלפון לא קיים כבר!"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_project__pricing_type
msgid "Pricing"
msgstr "תמחור"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_product_product
msgid "Product"
msgstr "מוצר"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_product_template
msgid "Product Template"
msgstr "תבנית מוצר "
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__professional_support
msgid "Professional Support"
msgstr "תמיכה מקצועית"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__project_manager_id
#: model_terms:ir.ui.view,arch_db:cor_custom.project_project_analytic_view_form
msgid "Project Manager"
msgstr "מנהל פרוייקט"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__project_name
msgid "Project Name"
msgstr "שם הפרוייקט"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_project_sale_line_employee_map
msgid "Project Sales line, employee mapping"
msgstr "קו מכירות פרויקטים, מיפוי עובדים"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_project__project_type
msgid "Project Type"
msgstr "סוג הפרויקט"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__project_type__no_limit
msgid "Projects that have no time limit"
msgstr "פרויקטים שאין להם מגבלת זמן"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_hr_employee_public
msgid "Public Employee"
msgstr "עובד ציבור"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.project_create_sale_order_inherit1_view_form
msgid "Qty"
msgstr "כמות"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_quotation_partner__action
msgid "Quotation Client"
msgstr "לקוח הצעת מחיר"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__ref_summary_status
msgid "Referral Summary status"
msgstr "מצב סיכום הפניות"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_sale_order
msgid "Sales Order"
msgstr "הזמנת לקוח"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__scope
msgid "Scope (NIS)"
msgstr "Scope (NIS)"
#. module: cor_custom
#: model_terms:ir.ui.view,arch_db:cor_custom.inherit_project_project_view_form
msgid "Service"
msgstr "שֵׁרוּת"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__start_date
msgid "Start Date"
msgstr "תאריך התחלה"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_account_analytic_line__start_time
msgid "Start Time"
msgstr "שעת התחלה"
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_account_analytic_line_check_start_time_positive
msgid "Start hour must be a positive number"
msgstr "שעת ההתחלה חייבת להיות מספר חיובי"
#. module: cor_custom
#: model:ir.model,name:cor_custom.model_project_project
msgid "Sub Project"
msgstr "פרויקט משנה"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_project__pricing_type
msgid ""
"The fixed rate is perfect if you bill a service at a fixed rate per hour or "
"day worked regardless of the consultant who performed it. The consultant "
"rate is preferable if your employees deliver the same service at a different"
" rate. For instance, junior and senior consultants would deliver the same "
"service (= consultancy), but at a different rate because of their level of "
"seniority."
msgstr ""
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_crm_lead__project_scope
msgid "The scope of the project"
msgstr "היקף הפרויקט"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_sale_line_employee_map__timesheet_hour
msgid "Timesheet Hour"
msgstr "שעת לוח הזמנים"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__project_project__project_type__hours_no_limit
msgid "Total hours are budgeted without division to consultant"
msgstr "Total hours are budgeted without division to consultant"
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_project_project__privacy_visibility
msgid "Visibility"
msgstr "יוצג ל:"
#. module: cor_custom
#: model:ir.model.fields,help:cor_custom.field_project_project__bill_type
msgid ""
"When billing tasks individually, a Sales Order will be created from each task. It is perfect if you would like to bill different services to different clients at different rates. \n"
" When billing the whole project, a Sales Order will be created from the project instead. This option is better if you would like to bill all the tasks of a given project to a specific client either at a fixed rate, or at an employee rate."
msgstr ""
#. module: cor_custom
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee__budgeted_hour_week
#: model:ir.model.fields,field_description:cor_custom.field_hr_employee_public__budgeted_hour_week
msgid "Working days per week"
msgstr "ימי עבודה בשבוע"
#. module: cor_custom
#: model:ir.model.fields.selection,name:cor_custom.selection__crm_lead__contract__yes
msgid "Yes"
msgstr "כן"
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_account_analytic_line_check_end_time_lower_than_24
msgid "You cannot have a end hour greater than 24"
msgstr "24 אינך יכול לקבל שעת סיום גדולה מ- "
#. module: cor_custom
#: model:ir.model.constraint,message:cor_custom.constraint_account_analytic_line_check_start_time_lower_than_24
msgid "You cannot have a start hour greater than 24"
msgstr "24 אינך יכול לקבל שעת התחלה גדולה מ- "
#. module: cor_custom
#: code:addons/cor_custom/models/analytic.py:0
#: code:addons/cor_custom/models/analytic.py:0
#, python-format
msgid "Your can not fill 0.0 hour entry"
msgstr "לא ניתן למלא כניסה של 0.0 שעות"
#. module: cor_custom
#: code:addons/cor_custom/models/analytic.py:0
#, python-format
msgid "Your can not fill entry more than Budgeted hours"
msgstr "אינך יכול למלא ערך יותר משעות שתוקצבו"

10
cor_custom/models/__init__.py Executable file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from . import crm_lead
from . import models
from . import project
from . import project_overview
from . import analytic
from . import product
from . import hr_employee
from . import sale

58
cor_custom/models/analytic.py Executable file
View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import api, exceptions, fields, models, _
from odoo.exceptions import UserError, AccessError, ValidationError
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
start_time = fields.Float(string='Start Time', digits=(16, 2))
end_time = fields.Float(string='End Time', digits=(16, 2))
_sql_constraints = [
('check_start_time_lower_than_24', 'CHECK(start_time <= 24)', 'You cannot have a start hour greater than 24'),
('check_start_time_positive', 'CHECK(start_time >= 0)', 'Start hour must be a positive number'),
('check_end_time_lower_than_24', 'CHECK(end_time <= 24)', 'You cannot have a end hour greater than 24'),
('check_end_time_positive', 'CHECK(end_time >= 0)', 'End hour must be a positive number'),
]
@api.constrains('start_time', 'end_time')
def _check_validity_start_time_end_time(self):
for rec in self:
if rec.start_time and rec.end_time:
if rec.end_time < rec.start_time:
raise exceptions.ValidationError(_('End time cannot be earlier than Start time'))
@api.onchange('start_time', 'end_time')
def _onchange_start_end_time(self):
if self.start_time > 0 and self.end_time > 0:
res = self.end_time - self.start_time
if res <= 0:
raise ValidationError(_("End time cannot be earlier than Start time"))
self.unit_amount = res
@api.model
def create(self, vals):
if vals.get('unit_amount') == 0.0:
raise ValidationError(_("Your can not fill 0.0 hour entry"))
if vals.get('employee_id') and vals.get('project_id'):
project = self.env['project.project'].search([('id', '=', vals.get('project_id'))])
if project:
for rec in project.sale_line_employee_ids:
if rec.employee_id.id == vals.get('employee_id'):
remain_hour = rec.budgeted_qty - rec.timesheet_hour
if vals.get('unit_amount') > remain_hour:
raise ValidationError(_("Your can not fill entry more than Budgeted hours"))
value = super(AccountAnalyticLine, self).create(vals)
return value
def write(self, vals):
if vals.get('unit_amount') == 0.0:
raise ValidationError(_("Your can not fill 0.0 hour entry"))
return super().write(vals)

49
cor_custom/models/crm_lead.py Executable file
View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class Lead(models.Model):
_inherit = 'crm.lead'
partner_id = fields.Many2one(
'res.partner', string='Client', index=True, tracking=10,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
lead_no = fields.Char(string='Lead ID')
scope = fields.Char(string='Scope (NIS)')
professional_support = fields.Char(string='Professional Support')
ref_summary_status = fields.Char(string='Referral Summary status')
contract = fields.Selection([('Yes','Yes'),('No','No')], string='Contract')
project_name = fields.Char(string='Project Name')
project_scope = fields.Char(string='The scope of the project')
act_project_manager_id = fields.Many2one('res.users', string='Actual Project Manager')
project_manager_id = fields.Many2one('res.users', string='Project Manager')
client_folder = fields.Char(string='Client Folder')
start_date = fields.Date(string='Start Date')
close_date = fields.Date(string='Close Date')
admin_user = fields.Boolean(compute='get_admin_login')
#user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
#set_readonly = fields.Boolean('Set Readonly', compute='_domain_check_user_group')
_sql_constraints = [
('lead_no_uniq', 'unique(lead_no)', "Lead ID already exists !"),
('phone_uniq', 'unique(phone)', "Phone No already exists !"),
('email_from_uniq', 'unique(email_from)', "Email ID already exists !"),
]
@api.onchange('type')
def get_admin_login(self):
#print('11111111111', self.env.context.get('uid'))
group_list = self.env.ref('base.group_erp_manager')
if (self.env.context.get('uid') in group_list.users.ids):
self.admin_user = True
else:
self.admin_user = False
"""def _domain_check_user_group(self):
print(self.user_has_groups('sales_team.group_sale_salesman'), self.user_has_groups('sales_team.group_sale_salesman_all_leads'), self.user_has_groups('group_sale_manager'))
if self.user_has_groups('sales_team.group_sale_salesman') and \
(not self.user_has_groups('sales_team.group_sale_salesman_all_leads') or not self.user_has_groups('group_sale_manager')):
self.set_readonly = True
return domain"""

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class HrEmployee(models.Model):
_inherit = 'hr.employee'
budgeted_hour_week = fields.Integer("Working days per week", default=5)
@api.onchange('resource_calendar_id')
def onchange_(self):
if self.resource_calendar_id and self.resource_calendar_id.attendance_ids:
total_week = [val.dayofweek for val in self.resource_calendar_id.attendance_ids]
res = list(set(total_week))
self.budgeted_hour_week = len(res)
class EmployeePublic(models.Model):
_inherit = 'hr.employee.public'
budgeted_hour_week = fields.Integer("Working days per week")

18
cor_custom/models/models.py Executable file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# from odoo import models, fields, api
# class cor_custom(models.Model):
# _name = 'cor_custom.cor_custom'
# _description = 'cor_custom.cor_custom'
# name = fields.Char()
# value = fields.Integer()
# value2 = fields.Float(compute="_value_pc", store=True)
# description = fields.Text()
#
# @api.depends('value')
# def _value_pc(self):
# for record in self:
# record.value2 = float(record.value) / 100

15
cor_custom/models/product.py Executable file
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class ProductTemplate(models.Model):
_inherit = "product.template"
taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', help="Default taxes used when selling the product.", string='Client Taxes',
domain=[('type_tax_use', '=', 'sale')], default=lambda self: self.env.company.account_sale_tax_id)
class ProductProduct(models.Model):
_inherit = "product.product"

135
cor_custom/models/project.py Executable file
View File

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class Project(models.Model):
_inherit = 'project.project'
bill_type = fields.Selection([
('customer_task', 'Invoice tasks separately to different clients'),
('customer_project', 'Invoice all tasks to a single client')
], string="Client Type", default="customer_project",
help='When billing tasks individually, a Sales Order will be created from each task. It is perfect if you would like to bill different services to different clients at different rates. \n When billing the whole project, a Sales Order will be created from the project instead. This option is better if you would like to bill all the tasks of a given project to a specific client either at a fixed rate, or at an employee rate.')
pricing_type = fields.Selection([
('fixed_rate', 'Fixed rate'),
('employee_rate', 'Consultant rate')
], string="Pricing", default="fixed_rate",
help='The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the consultant who performed it. The consultant rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.')
project_type = fields.Selection([
('hours_in_consultant', 'Hours are budgeted according to a consultant'),
('hours_no_limit', 'Total hours are budgeted without division to consultant'),
], string="Project Type", default="hours_in_consultant")
privacy_visibility = fields.Selection([
('followers', 'Invited internal users'),
('employees', 'All internal users'),
('portal', 'Invited portal users and all internal users'),
],
string='Visibility', required=True,
default='followers',
help="Defines the visibility of the tasks of the project:\n"
"- Invited internal users: employees may only see the followed project and tasks.\n"
"- All internal users: employees may see all project and tasks.\n"
"- Invited portal and all internal users: employees may see everything."
" Portal users may see project and tasks followed by\n"
" them or by someone of their company.")
allow_billable = fields.Boolean("Billable", default=True, help="Invoice your time and material from tasks.")
budgeted_hours = fields.Float(string='Total Budgeted Hours', digits=(16, 2))
budgeted_revenue = fields.Float(string='Budgeted Revenue', digits=(16, 2))
expenses_per = fields.Float(string='Expenses (%)', digits=(16, 2))
expenses_amt = fields.Float(string='Expenses Amount', digits=(16, 2))
cost = fields.Float("Sum of Cost", compute='onchange_compute_values', store=True)
hourly_rate = fields.Float("Sum of Hourly rate", compute='onchange_compute_values', store=True)
budgeted_hour_week = fields.Float("Budgeted Hours(per week)", compute='onchange_compute_values', store=True)
total_expenses = fields.Float(string='Total Expenses', digits=(16, 2), compute='_compute_calc', store=True)
profit_amt = fields.Float(string='Profit Amount', digits=(16, 2), compute='_compute_calc', store=True)
profit_per = fields.Float(string='Porfit Percentage', digits=(16, 2), compute='_compute_calc', store=True)
@api.onchange('budgeted_revenue', 'expenses_per')
def onchange_expenses_per(self):
if self.budgeted_revenue > 0 and self.expenses_per > 0:
expense_amount = self.budgeted_revenue * (self.expenses_per / 100)
self.expenses_amt = expense_amount
@api.depends('cost', 'expenses_amt', 'budgeted_revenue')
def _compute_calc(self):
for record in self:
total_exp = record.cost + record.expenses_amt
record.total_expenses = total_exp
profit_amt = record.budgeted_revenue - total_exp
record.profit_amt = profit_amt
if record.profit_amt > 0 and record.budgeted_revenue > 0:
record.profit_per = (record.profit_amt / record.budgeted_revenue) * 100
@api.depends('sale_line_employee_ids')
def onchange_compute_values(self):
for record in self:
if record.project_type == 'hours_in_consultant':
val = 0.0
cost = 0.0
budgeted_hour_week = 0.0
for rec in record.sale_line_employee_ids:
val = val + rec.budgeted_qty
cost = cost + rec.cost
budgeted_hour_week = budgeted_hour_week + rec.budgeted_hour_week
record.budgeted_hours = val
record.cost = cost
if val > 0.0:
record.hourly_rate = (cost/val)
record.budgeted_hour_week = budgeted_hour_week
class InheritProjectProductEmployeeMap(models.Model):
_inherit = 'project.sale.line.employee.map'
employee_price = fields.Monetary(string="Consultant Price", related="employee_id.timesheet_cost", readonly=True)
budgeted_qty = fields.Float(string='Budgeted Hours', store=True)
budgeted_uom = fields.Many2one('uom.uom', string='Budgeted UOM', related='sale_line_id.product_uom', readonly=True)
# budgeted_uom = fields.Many2one('uom.uom', string='Budgeted UOM', related='timesheet_product_id.uom_id', readonly=True)
timesheet_hour = fields.Float("Timesheet Hour", compute='_compute_timesheet_hour', default=0.0)
budgeted_hour_week = fields.Float("Budgeted Hours per week", compute='_compute_budgeted_hour_week')
price_unit = fields.Float("Hourly rate")
currency_id = fields.Many2one('res.currency', string="Currency", compute='_compute_price_unit', store=True,
readonly=False)
sale_line_id = fields.Many2one('sale.order.line', "Service", domain=[('is_service', '=', True)])
cost = fields.Float("Cost", compute='_compute_total_cost')
def _compute_timesheet_hour(self):
for val in self:
self._cr.execute('''SELECT project_id, employee_id, SUM(unit_amount) FROM account_analytic_line
where project_id = %(project_id)s and employee_id = %(employee_id)s
GROUP BY project_id, employee_id''',
{'project_id': val.project_id._origin.id, 'employee_id': val.employee_id.id, })
res = self._cr.fetchone()
if res and res[2]:
val.timesheet_hour = res[2]
else:
val.timesheet_hour = 0.0
def _compute_budgeted_hour_week(self):
for val in self:
if val.employee_id and val.employee_id.budgeted_hour_week > 0 and val.budgeted_qty:
val.budgeted_hour_week = (val.budgeted_qty / val.employee_id.budgeted_hour_week)
else:
val.budgeted_hour_week = 0
def _compute_total_cost(self):
for val in self:
val.cost = val.budgeted_qty * val.price_unit
@api.depends('sale_line_id', 'sale_line_id.price_unit', 'timesheet_product_id')
def _compute_price_unit(self):
for line in self:
if line.sale_line_id:
line.price_unit = line.sale_line_id.price_unit
line.currency_id = line.sale_line_id.currency_id
elif line.timesheet_product_id:
line.price_unit = line.timesheet_product_id.lst_price
line.currency_id = line.timesheet_product_id.currency_id
else:
#line.price_unit = 0
line.currency_id = False

View File

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
import babel.dates
from dateutil.relativedelta import relativedelta
import itertools
import json
from odoo import fields, _, models
from odoo.osv import expression
from odoo.tools import float_round
from odoo.tools.misc import get_lang
from odoo.addons.web.controllers.main import clean_action
DEFAULT_MONTH_RANGE = 3
class Project(models.Model):
_inherit = 'project.project'
def _plan_prepare_values(self):
currency = self.env.company.currency_id
uom_hour = self.env.ref('uom.product_uom_hour')
company_uom = self.env.company.timesheet_encode_uom_id
is_uom_day = company_uom == self.env.ref('uom.product_uom_day')
hour_rounding = uom_hour.rounding
billable_types = ['non_billable', 'non_billable_project', 'billable_time', 'non_billable_timesheet', 'billable_fixed']
values = {
'projects': self,
'currency': currency,
'timesheet_domain': [('project_id', 'in', self.ids)],
'profitability_domain': [('project_id', 'in', self.ids)],
'stat_buttons': self._plan_get_stat_button(),
'is_uom_day': is_uom_day,
}
#
# Hours, Rates and Profitability
#
dashboard_values = {
'time': dict.fromkeys(billable_types + ['total'], 0.0),
'rates': dict.fromkeys(billable_types + ['total'], 0.0),
'profit': {
'invoiced': 0.0,
'to_invoice': 0.0,
'cost': 0.0,
'total': 0.0,
}
}
# hours from non-invoiced timesheets that are linked to canceled so
canceled_hours_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), ('so_line.state', '=', 'cancel')]
total_canceled_hours = sum(self.env['account.analytic.line'].search(canceled_hours_domain).mapped('unit_amount'))
canceled_hours = float_round(total_canceled_hours, precision_rounding=hour_rounding)
if is_uom_day:
# convert time from hours to days
canceled_hours = round(uom_hour._compute_quantity(canceled_hours, company_uom, raise_if_failure=False), 2)
dashboard_values['time']['canceled'] = canceled_hours
dashboard_values['time']['total'] += canceled_hours
# hours (from timesheet) and rates (by billable type)
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) + total_canceled_hours
for data in dashboard_data:
billable_type = data['timesheet_invoice_type']
amount = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
if is_uom_day:
# convert time from hours to days
amount = round(uom_hour._compute_quantity(amount, company_uom, raise_if_failure=False), 2)
dashboard_values['time'][billable_type] = amount
dashboard_values['time']['total'] += amount
# rates
rate = round(data.get('unit_amount') / dashboard_total_hours * 100, 2) if dashboard_total_hours else 0.0
dashboard_values['rates'][billable_type] = rate
dashboard_values['rates']['total'] += rate
dashboard_values['time']['total'] = round(dashboard_values['time']['total'], 2)
# rates from non-invoiced timesheets that are linked to canceled so
dashboard_values['rates']['canceled'] = float_round(100 * total_canceled_hours / (dashboard_total_hours or 1), precision_rounding=hour_rounding)
other_revenues = self.env['account.analytic.line'].read_group([
('account_id', 'in', self.analytic_account_id.ids),
('amount', '>=', 0),
('project_id', '=', False)], ['amount'], [])[0].get('amount', 0)
# profitability, using profitability SQL report
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0)
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id'])
for data in profitability_raw_data:
profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0)
profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0)
profit['cost'] += data.get('timesheet_cost', 0.0)
profit['expense_cost'] += data.get('expense_cost', 0.0)
profit['expense_amount_untaxed_invoiced'] += data.get('expense_amount_untaxed_invoiced', 0.0)
profit['other_revenues'] = other_revenues or 0
profit['total'] = sum([profit[item] for item in profit.keys()])
# Profit Percentage added in COR Project
try:
profit_amount = profit['total']
revenues_amount = profit['invoiced'] + profit['to_invoice'] + profit['other_revenues']
profit_percent = round(profit_amount / revenues_amount * 100, 2) if revenues_amount else 0.0
profit['profit_percent'] = profit_percent
except Exception:
profit['profit_percent'] = 0
# End
dashboard_values['profit'] = profit
values['dashboard'] = dashboard_values
#
# Time Repartition (per employee per billable types)
#
user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id'])
user_ids = [user_id['user_id'][0] for user_id in user_ids]
employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids'])
# flatten the list of list
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
# Retrieve the employees for which the current user can see theirs timesheets
employee_domain = expression.AND([[('company_id', 'in', self.env.companies.ids)], self.env['account.analytic.line']._domain_employee_id()])
employees = self.env['hr.employee'].sudo().browse(employee_ids).filtered_domain(employee_domain)
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
# repartition data, without timesheet on cancelled so
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
# read timesheet on cancelled so
cancelled_so_timesheet = self.env['account.analytic.line'].read_group(repartition_domain + [('so_line.state', '=', 'cancel')], ['employee_id', 'unit_amount'], ['employee_id'], lazy=False)
repartition_data += [{**canceled, 'timesheet_invoice_type': 'canceled'} for canceled in cancelled_so_timesheet]
# set repartition per type per employee
repartition_employee = {}
for employee in employees:
repartition_employee[employee.id] = dict(
employee_id=employee.id,
employee_name=employee.name,
employee_price=employee.timesheet_cost,
non_billable_project=0.0,
non_billable=0.0,
billable_time=0.0,
non_billable_timesheet=0.0,
billable_fixed=0.0,
canceled=0.0,
total=0.0,
)
for data in repartition_data:
employee_id = data['employee_id'][0]
repartition_employee.setdefault(employee_id, dict(
employee_id=data['employee_id'][0],
employee_name=data['employee_id'][1],
employee_price=data['employee_id'][1],
non_billable_project=0.0,
non_billable=0.0,
billable_time=0.0,
non_billable_timesheet=0.0,
billable_fixed=0.0,
canceled=0.0,
total=0.0,
))[data['timesheet_invoice_type']] = float_round(data.get('unit_amount', 0.0), precision_rounding=hour_rounding)
repartition_employee[employee_id]['__domain_' + data['timesheet_invoice_type']] = data['__domain']
# compute total
for employee_id, vals in repartition_employee.items():
repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in [*billable_types, 'canceled']])
if is_uom_day:
# convert all times from hours to days
for time_type in ['non_billable_project', 'non_billable', 'billable_time', 'non_billable_timesheet', 'billable_fixed', 'canceled', 'total']:
if repartition_employee[employee_id][time_type]:
repartition_employee[employee_id][time_type] = round(uom_hour._compute_quantity(repartition_employee[employee_id][time_type], company_uom, raise_if_failure=False), 2)
hours_per_employee = [repartition_employee[employee_id]['total'] for employee_id in repartition_employee]
values['repartition_employee_max'] = (max(hours_per_employee) if hours_per_employee else 1) or 1
values['repartition_employee'] = repartition_employee
#
# Table grouped by SO / SOL / Employees
#
timesheet_forecast_table_rows = self._table_get_line_values(employees)
if timesheet_forecast_table_rows:
values['timesheet_forecast_table'] = timesheet_forecast_table_rows
return values

13
cor_custom/models/sale.py Executable file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class SaleInherit(models.Model):
_inherit = 'sale.order'
partner_id = fields.Many2one(
'res.partner', string='Client', readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
required=True, change_default=True, index=True, tracking=1,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)

4
cor_custom/report/__init__.py Executable file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project_profitability_report_analysis

View File

@ -0,0 +1,360 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, tools
class ProfitabilityAnalysis(models.Model):
_name = "project.profitability.report"
_description = "Project Profitability Report"
_order = 'project_id, sale_line_id'
_auto = False
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True)
project_id = fields.Many2one('project.project', string='Project', readonly=True)
currency_id = fields.Many2one('res.currency', string='Project Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Project Company', readonly=True)
user_id = fields.Many2one('res.users', string='Project Manager', readonly=True)
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
line_date = fields.Date("Date", readonly=True)
# cost
#budgeted_unit_amount = fields.Float("Budgeted Duration", digits=(16, 2), readonly=True, group_operator="sum")
timesheet_unit_amount = fields.Float("Timesheet Duration", digits=(16, 2), readonly=True, group_operator="sum")
timesheet_cost = fields.Float("Timesheet Cost", digits=(16, 2), readonly=True, group_operator="sum")
expense_cost = fields.Float("Other Costs", digits=(16, 2), readonly=True, group_operator="sum")
# sale revenue
order_confirmation_date = fields.Datetime('Sales Order Confirmation Date', readonly=True)
sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True)
sale_order_id = fields.Many2one('sale.order', string='Sale Order', readonly=True)
product_id = fields.Many2one('product.product', string='Product', readonly=True)
amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Invoice", digits=(16, 2), readonly=True, group_operator="sum")
amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_invoiced = fields.Float("Untaxed Amount Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum")
other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum",
help="All revenues that are not from timesheets and that are linked to the analytic account of the project.")
margin = fields.Float("Margin", digits=(16, 2), readonly=True, group_operator="sum")
def init(self):
tools.drop_view_if_exists(self._cr, self._table)
query = """
CREATE VIEW %s AS (
SELECT
sub.id as id,
sub.project_id as project_id,
sub.user_id as user_id,
sub.sale_line_id as sale_line_id,
sub.analytic_account_id as analytic_account_id,
sub.partner_id as partner_id,
sub.company_id as company_id,
sub.currency_id as currency_id,
sub.sale_order_id as sale_order_id,
sub.order_confirmation_date as order_confirmation_date,
sub.product_id as product_id,
sub.sale_qty_delivered_method as sale_qty_delivered_method,
sub.expense_amount_untaxed_to_invoice as expense_amount_untaxed_to_invoice,
sub.expense_amount_untaxed_invoiced as expense_amount_untaxed_invoiced,
sub.amount_untaxed_to_invoice as amount_untaxed_to_invoice,
sub.amount_untaxed_invoiced as amount_untaxed_invoiced,
sub.timesheet_unit_amount as timesheet_unit_amount,
sub.timesheet_cost as timesheet_cost,
sub.expense_cost as expense_cost,
sub.other_revenues as other_revenues,
sub.line_date as line_date,
(sub.expense_amount_untaxed_to_invoice + sub.expense_amount_untaxed_invoiced + sub.amount_untaxed_to_invoice +
sub.amount_untaxed_invoiced + sub.other_revenues + sub.timesheet_cost + sub.expense_cost)
as margin
FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id,
P.id AS project_id,
P.user_id AS user_id,
SOL.id AS sale_line_id,
P.analytic_account_id AS analytic_account_id,
P.partner_id AS partner_id,
C.id AS company_id,
C.currency_id AS currency_id,
S.id AS sale_order_id,
S.date_order AS order_confirmation_date,
SOL.product_id AS product_id,
SOL.qty_delivered_method AS sale_qty_delivered_method,
COST_SUMMARY.expense_amount_untaxed_to_invoice AS expense_amount_untaxed_to_invoice,
COST_SUMMARY.expense_amount_untaxed_invoiced AS expense_amount_untaxed_invoiced,
COST_SUMMARY.amount_untaxed_to_invoice AS amount_untaxed_to_invoice,
COST_SUMMARY.amount_untaxed_invoiced AS amount_untaxed_invoiced,
COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount,
COST_SUMMARY.timesheet_cost AS timesheet_cost,
COST_SUMMARY.expense_cost AS expense_cost,
COST_SUMMARY.other_revenues AS other_revenues,
COST_SUMMARY.line_date::date AS line_date
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
-- Each costs and revenues will be retrieved individually by sub-requests
-- This is required to able to get the date
SELECT
project_id,
analytic_account_id,
sale_line_id,
SUM(timesheet_unit_amount) AS timesheet_unit_amount,
SUM(timesheet_cost) AS timesheet_cost,
SUM(expense_cost) AS expense_cost,
SUM(other_revenues) AS other_revenues,
SUM(downpayment_invoiced) AS downpayment_invoiced,
SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
SUM(amount_untaxed_invoiced) AS amount_untaxed_invoiced,
line_date AS line_date
FROM (
-- Get the timesheet costs
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
TS.so_line AS sale_line_id,
TS.unit_amount AS timesheet_unit_amount,
TS.amount AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
TS.date AS line_date
FROM account_analytic_line TS, project_project P
WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the other revenues
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
AAL.amount AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
AAL.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the expense costs from account analytic line
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
AAL.amount AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
AAL.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the invoiced downpayments
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
MY_S.date_order AS line_date
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
UNION ALL
-- Get the expense costs from sale order line
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
ANLI.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the following values: expense amount untaxed to invoice/invoiced, amount untaxed to invoice/invoiced
-- These values have to be computed from all the records retrieved just above but grouped by project and sale order line
SELECT
AMOUNT_UNTAXED.project_id AS project_id,
AMOUNT_UNTAXED.analytic_account_id AS analytic_account_id,
AMOUNT_UNTAXED.sale_line_id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
CASE
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS expense_amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
THEN
CASE
WHEN T.expense_policy = 'sales_price'
THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced
ELSE -AMOUNT_UNTAXED.expense_cost
END
ELSE 0.0
END AS expense_amount_untaxed_invoiced,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, AMOUNT_UNTAXED.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS amount_untaxed_invoiced,
S.date_order AS line_date
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM account_analytic_line AAL, project_project P
WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
GROUP BY P.id, AAL.so_line
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, AA.id, AAL.so_line
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
SUM(AAL.amount) AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, AA.id, AAL.so_line
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
GROUP BY P.id, MY_SOLS.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, OLIS.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM sale_order_line SOL
INNER JOIN project_project P ON SOL.project_id = P.id
WHERE P.active = 't' AND P.allow_timesheets = 't'
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM sale_order_line SOL
INNER JOIN project_task T ON SOL.task_id = T.id
INNER JOIN project_project P ON P.id = T.project_id
WHERE P.active = 't' AND P.allow_timesheets = 't'
) AMOUNT_UNTAXED ON AMOUNT_UNTAXED.project_id = P.id
LEFT JOIN sale_order_line SOL ON AMOUNT_UNTAXED.sale_line_id = SOL.id
LEFT JOIN sale_order S ON SOL.order_id = S.id
LEFT JOIN product_product PP on (SOL.product_id = PP.id)
LEFT JOIN product_template T on (PP.product_tmpl_id = T.id)
WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
) SUB_COST_SUMMARY
GROUP BY project_id, analytic_account_id, sale_line_id, line_date
) COST_SUMMARY ON COST_SUMMARY.project_id = P.id
LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id
LEFT JOIN sale_order S ON SOL.order_id = S.id
WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
) AS sub
)
""" % self._table
self._cr.execute(query)

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_timesheet.project_profitability_report_view_pivot" model="ir.ui.view">
<field name="name">project.profitability.report.pivot</field>
<field name="model">project.profitability.report</field>
<field name="arch" type="xml">
<pivot string="Profitability Analysis" display_quantity="true" disable_linking="True" sample="1">
<field name="project_id" type="row"/>
<field name="amount_untaxed_to_invoice" type="measure"/>
<field name="amount_untaxed_invoiced" type="measure"/>
<field name="timesheet_cost" type="measure"/>
<!--<field name="budgeted_unit_amount" type="measure"/>-->
</pivot>
</field>
</record>
<record id="sale_timesheet.project_profitability_report_view_graph" model="ir.ui.view">
<field name="name">project.profitability.report.graph</field>
<field name="model">project.profitability.report</field>
<field name="arch" type="xml">
<graph string="Profitability Analysis" type="bar" sample="1" disable_linking="1">
<field name="project_id" type="row"/>
<field name="product_id" type="col"/>
<field name="amount_untaxed_to_invoice" type="measure"/>
<field name="amount_untaxed_invoiced" type="measure"/>
<field name="timesheet_cost" type="measure"/>
<field name="other_revenues" type="measure"/>
<field name="margin" type="measure"/>
</graph>
</field>
</record>
<record id="sale_timesheet.project_profitability_report_view_search" model="ir.ui.view">
<field name="name">project.profitability.report.search</field>
<field name="model">project.profitability.report</field>
<field name="arch" type="xml">
<search string="Profitability Analysis">
<field name="project_id"/>
<field name="user_id"/>
<field name="product_id"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="company_id" groups="base.group_multi_company"/>
<filter string="My Projects" name="my_project" domain="[('user_id','=', uid)]"/>
<group expand="1" string="Group By">
<filter string="Project" name="group_by_project" context="{'group_by':'project_id'}"/>
<filter string="Project Manager" name="group_by_user_id" context="{'group_by':'user_id'}"/>
<filter string="Customer" name="group_by_partner_id" context="{'group_by':'partner_id'}"/>
<filter string="Company" name="group_by_company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Date" name="group_by_line_date" context="{'group_by':'line_date'}"/>
</group>
</search>
</field>
</record>
<record id="sale_timesheet.project_profitability_report_action" model="ir.actions.act_window">
<field name="name">Project Costs and Revenues</field>
<field name="res_model">project.profitability.report</field>
<field name="view_mode">pivot,graph</field>
<field name="search_view_id" ref="sale_timesheet.project_profitability_report_view_search"/>
<field name="context">{
'group_by_no_leaf':1,
'group_by':[],
'sale_show_order_product_name': 1,
}</field>
<field name="help">This report allows you to analyse the profitability of your projects: compare the amount to invoice, the ones already invoiced and the project cost (via timesheet cost of your employees).</field>
</record>
<menuitem id="sale_timesheet.menu_project_profitability_analysis"
parent="project.menu_project_report"
action="sale_timesheet.project_profitability_report_action"
name="Project Costs and Revenues"
sequence="50"/>
</odoo>

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cor_custom_cor_custom,cor_custom.cor_custom,model_cor_custom_cor_custom,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_cor_custom_cor_custom cor_custom.cor_custom model_cor_custom_cor_custom base.group_user 1 1 1 1

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--<record id="view_account_analytic_line_inherit_percentage" model="ir.ui.view">
<field name="name">Percentage Analytic</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="analytic.view_account_analytic_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='currency_id']" position="after">
<separator string="Percentage"/>
<field name="ispercentage"/>
<field name="percentage_rate" attrs="{'invisible':[('ispercentage','=',False)]}"/>
<field name="per_from_amt" attrs="{'invisible':[('ispercentage','=',False)]}"/>
</xpath>
</field>
</record>-->
<record id="hr_timesheet_line_tree_inherit1" model="ir.ui.view">
<field name="name">account.analytic.line.tree.hr_timesheet_inherit1</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="start_time" widget="float_time"/>
<field name="end_time" widget="float_time"/>
</xpath>
</field>
</record>
<!--<record id="timesheet_view_tree_user_inherit1" model="ir.ui.view">
<field name="name">account.analytic.line.tree.hr_timesheet_user_inherit1</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.timesheet_view_tree_user"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="start_time"/>
<field name="end_time"/>
</xpath>
</field>
</record>-->
</odoo>

88
cor_custom/views/crm_view.xml Executable file
View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crm_lead_form_cor_custom" model="ir.ui.view">
<field name="name">crm.lead.cor.inherit1</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='opportunity_partner']//field[@name='partner_id']" position="replace">
<field name="partner_id"
widget="res_partner_many2one"
string="Client"
context="{'res_partner_search_mode': type == 'opportunity' and 'customer' or False,
'default_name': contact_name or partner_name,
'default_street': street,
'default_is_company': type == 'opportunity' and contact_name == False,
'default_company_name': type == 'opportunity' and partner_name,
'default_street2': street2,
'default_city': city,
'default_title': title,
'default_state_id': state_id,
'default_zip': zip,
'default_country_id': country_id,
'default_function': function,
'default_phone': phone,
'default_mobile': mobile,
'default_email': email_from,
'default_user_id': user_id,
'default_team_id': team_id,
'default_website': website,
'show_vat': True,
}"
/>
</xpath>
<!--<xpath expr="//field[@name='user_id']" position="replace">
<field name="set_readonly" invisible="1" />
<field name="user_id" domain="[('share', '=', False)]" attrs="{'readonly': [('set_readonly','!=',True)]}"
context="{'default_sales_team_id': team_id}" widget="many2one_avatar_user"/>
</xpath>-->
<xpath expr="//group[@name='Misc']" position="after">
<group colspan="2" col="4">
<field name="lead_no"/>
<field name="scope"/>
<field name="professional_support"/>
<field name="ref_summary_status"/>
<field name="contract"/>
<field name="client_folder"/>
<field name="start_date"/>
<field name="close_date"/>
<field name="project_name"/>
<field name="project_scope"/>
<field name="act_project_manager_id"/>
<field name="project_manager_id"/>
</group>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="admin_user" invisible="1"/>
<field name="user_id" domain="[('share', '=', False)]" attrs="{'readonly':[('admin_user','=',False)]}"
context="{'default_sales_team_id': team_id}" widget="many2one_avatar_user"/>
</xpath>
</field>
</record>
<!-- <record id="view_salesperson_readonly_custom" model="ir.ui.view">
<field name="name">crm.lead.cor.inherit2</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="cor_custom.crm_lead_form_cor_custom"/>
<field name="groups_id" eval="[(6, 0, [ref('base.group_erp_manager')])]"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="attributes">
<attribute name="readonly">False</attribute>
</xpath>
</field>
</record>
-->
<record id="crm_case_kanban_view_leads" model="ir.ui.view">
<field name="name">crm.lead.kanban.lead</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_kanban_view_leads"/>
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="on_create"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="inherit_view_employee_form" model="ir.ui.view">
<field name="name">hr.employee.form.inherit</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='resource_calendar_id']" position="after">
<field name="budgeted_hour_week"/>
</xpath>
</field>
</record>
<!--<record id="inherit_view_employee_form" model="ir.ui.view">
<field name="name">hr.employee.form.inherit</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='hr_settings']" position="inside">
<group>
<label for="budgeted_hour_week"/>
<div>
<field name="budgeted_hour_week" class="oe_inline"/>
per week
</div>
</group>
</xpath>
</field>
</record>-->
</odoo>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="inherit_timesheet_plan" model="ir.ui.view">
<field name="name">Timesheet Plan</field>
<field name="type">qweb</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="sale_timesheet.timesheet_plan"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_profitability_wrapper')]//div[hasclass('o_profitability_section')][2]//tr[7]"
position="replace">
<tr>
<td class="o_timesheet_plan_dashboard_total">
<b>
<t t-esc="dashboard['profit']['total']"
t-options='{"widget": "monetary", "display_currency": currency}'/>
</b>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['profit']['profit_percent']"/> %)
</td>
<td>
<b>Total</b>
</td>
</tr>
</xpath>
<xpath expr="//div[@class='o_timesheet_plan_sale_timesheet_people_time']/t/div/table/thead/tr/th[2]"
position="after">
<th>Hourly Rate</th>
</xpath>
<xpath expr="//div[@class='o_timesheet_plan_sale_timesheet_people_time']/t/div/table/tbody/t/tr/td[2]"
position="after">
<td style="width: 15%; vertical-align: middle;">
<t t-esc="employee['employee_price']"/>
</td>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail.menu_root_discuss" model="ir.ui.menu">
<field name="groups_id" eval="[(6, 0, [ref('cor_custom.group_show_hr_discuss_group')])]"/>
</record>
<record id="contacts.menu_contacts" model="ir.ui.menu">
<field name="groups_id" eval="[(6, 0, [ref('cor_custom.group_show_hr_contact_group')])]"/>
</record>
<record id="hr.menu_hr_root" model="ir.ui.menu">
<field name="groups_id" eval="[(6, 0, [ref('cor_custom.group_show_hr_menu_group')])]"/>
</record>
</odoo>

139
cor_custom/views/project_view.xml Executable file
View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_project_analytic_view_form" model="ir.ui.view">
<field name="name">Project Analytic</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button" type="object" name="action_view_account_analytic_line" icon="fa-usd"
attrs="{'invisible': [('allow_billable','=',False)]}" string="Cost/Revenue" widget="statinfo">
</button>
</div>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" string="Project Manager" widget="many2one_avatar_user" required="0"
attrs="{'readonly':[('active','=',False)]}" domain="[('share', '=', False)]"/>
</xpath>
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" string="Client" required="0"/>
</xpath>
</field>
</record>
<record id="inherit_project_project_view_form" model="ir.ui.view">
<field name="name">project.project.form.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="sale_timesheet.project_project_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='billing_employee_rate']" position="replace">
<page name="billing_employee_rate" string="Invoicing"
attrs="{'invisible': [('allow_billable', '=', False)]}">
<group>
<field name="display_create_order" invisible="1"/>
<!--<field name="project_type" widget="radio"/>-->
<field name="bill_type" widget="radio" invisible="1"/>
<field name="pricing_type"
attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}"
widget="radio"/>
<field name="project_type" attrs="{'invisible': [('pricing_type', '=', 'fixed_rate')]}"
widget="radio"/>
<field name="budgeted_revenue"
attrs="{'invisible': [('pricing_type','=','fixed_rate')], 'required': [('pricing_type','!=','fixed_rate')]}"/>
<field name="expenses_per" attrs="{'invisible': [('pricing_type','=','fixed_rate')]}"/>
<field name="expenses_amt" attrs="{'invisible': [('pricing_type','=','fixed_rate')]}"/>
<div class="o_td_label"
attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}">
<label for="timesheet_product_id" string="Default Service"
attrs="{'invisible': [('bill_type', '!=', 'customer_task')]}"/>
<label for="timesheet_product_id" string="Service"
attrs="{'invisible': [('bill_type', '=', 'customer_task')]}"/>
</div>
<field name="timesheet_product_id" nolabel="1"
attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}"
context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
<field name="sale_order_id" invisible="1"/>
<field name="sale_line_id" string="Default Sales Order Item"
attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}"
options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
</group>
<field name="sale_line_employee_ids"
attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}">
<tree editable="top">
<field name="company_id" invisible="1"/>
<field name="project_id" invisible="1"/>
<field name="employee_id" options="{'no_create': True}" string="Consultant Name"/>
<!--<field name="sale_line_id" options="{'no_create': True}"
attrs="{'column_invisible': [('parent.sale_order_id', '=', False)]}"
domain="[('order_id','=',parent.sale_order_id), ('is_service', '=', True)]"/>
<field name="price_unit" widget="monetary" options="{'currency_field': 'currency_id'}"
attrs="{'readonly': [('parent.sale_order_id', '!=', False)]}"/>-->
<field name="price_unit" readonly="0"/>
<field name="currency_id" invisible="1"/>
<field name="budgeted_qty"
attrs="{'readonly': [('parent.project_type', '=', 'hours_no_limit')]}"/>
<field name="cost"/>
<field name="timesheet_hour"/>
<field name="budgeted_hour_week"/>
<!--<field name="timesheet_product_id"
attrs="{'column_invisible': [('parent.sale_order_id', '!=', False)]}"/>-->
<!--<field name="budgeted_uom"
attrs="{'column_invisible': [('parent.sale_order_id', '=', False)]}"/>-->
<!--<field name="budgeted_qty"
attrs="{'column_invisible': [('parent.sale_order_id', '=', False)]}"/>-->
<!--<field name="employee_price" widget="monetary" options="{'currency_field': 'currency_id'}"/>-->
</tree>
</field>
<group attrs="{'invisible': [('pricing_type','=','fixed_rate')]}">
<group>
<field name="budgeted_hours"
attrs="{'invisible': [('pricing_type','=','fixed_rate')], 'readonly': [('project_type','=','hours_in_consultant')],
'required': [('pricing_type','!=','fixed_rate')]}"/>
<field name="total_expenses"/>
<field name="profit_amt"/>
<field name="profit_per"/>
</group>
<group>
<field name="cost"/>
<field name="hourly_rate"/>
<field name="budgeted_hour_week"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
<record id="project.open_view_project_all" model="ir.actions.act_window">
<field name="name">Projects</field>
<field name="res_model">project.project</field>
<field name="domain">[]</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="project.view_project"/>
<field name="search_view_id" ref="project.view_project_project_filter"/>
<field name="target">main</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No projects found. Let's create one!
</p>
<p>
Projects regroup tasks on the same topic and each have their own dashboard.
</p>
</field>
</record>
<record id="inherit_view_task_form2" model="ir.ui.view">
<field name="name">project.task.form.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" class="o_task_customer_field" string="Client"/>
</xpath>
</field>
</record>
</odoo>

51
cor_custom/views/sale_views.xml Executable file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="inherit_view_order_form" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!--<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" widget="res_partner_many2one" string="Client" required="1"
context="{'res_partner_search_mode': 'customer', 'show_address': 1, 'show_vat': True}"
options='{"always_reload": True}'/>
</xpath>-->
<xpath expr="//field[@name='require_signature']" position="replace">
<field name="require_signature" invisible="1"/>
</xpath>
<xpath expr="//field[@name='require_payment']" position="replace">
<field name="require_payment" invisible="1"/>
</xpath>
<xpath expr="//field[@name='payment_term_id']" position="replace">
<field name="payment_term_id" invisible="1" options="{'no_open':True,'no_create': True}"/>
</xpath>
<page name="customer_signature" position="attributes">
<attribute name='invisible'>1</attribute>
</page>
</field>
</record>
<!-- <record id="inherit_view_quotation_tree" model="ir.ui.view">
<field name="name">sale.order.tree.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_quotation_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" readonly="1" string="Client"/>
</xpath>
</field>
</record>
<record id="inherit_view_order_tree" model="ir.ui.view">
<field name="name">sale.order.tree.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" readonly="1" string="Client"/>
</xpath>
</field>
</record> -->
</odoo>

24
cor_custom/views/templates.xml Executable file
View File

@ -0,0 +1,24 @@
<odoo>
<data>
<!--
<template id="listing">
<ul>
<li t-foreach="objects" t-as="object">
<a t-attf-href="#{ root }/objects/#{ object.id }">
<t t-esc="object.display_name"/>
</a>
</li>
</ul>
</template>
<template id="object">
<h1><t t-esc="object.display_name"/></h1>
<dl>
<t t-foreach="object._fields" t-as="field">
<dt><t t-esc="field"/></dt>
<dd><t t-esc="object[field]"/></dd>
</t>
</dl>
</template>
-->
</data>
</odoo>

60
cor_custom/views/views.xml Executable file
View File

@ -0,0 +1,60 @@
<odoo>
<data>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="cor_custom.list">
<field name="name">cor_custom list</field>
<field name="model">cor_custom.cor_custom</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="cor_custom.action_window">
<field name="name">cor_custom window</field>
<field name="res_model">cor_custom.cor_custom</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="cor_custom.action_server">
<field name="name">cor_custom server</field>
<field name="model_id" ref="model_cor_custom_cor_custom"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="cor_custom" id="cor_custom.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="cor_custom.menu_1" parent="cor_custom.menu_root"/>
<menuitem name="Menu 2" id="cor_custom.menu_2" parent="cor_custom.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="cor_custom.menu_1_list" parent="cor_custom.menu_1"
action="cor_custom.action_window"/>
<menuitem name="Server to list" id="cor_custom" parent="cor_custom.menu_2"
action="cor_custom.action_server"/>
-->
</data>
</odoo>

5
cor_custom/wizard/__init__.py Executable file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project_create_sale_order
from . import crm_opportunity_to_quotation

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class Opportunity2Quotation(models.TransientModel):
_inherit = 'crm.quotation.partner'
action = fields.Selection([
('create', 'Create a new client'),
('exist', 'Link to an existing client'),
('nothing', 'Do not link to a client')
], string='Quotation Client', required=True)
lead_id = fields.Many2one('crm.lead', "Associated Lead", required=True)
partner_id = fields.Many2one('res.partner', 'Client')

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class ProjectCreateSalesOrder(models.TransientModel):
_inherit = 'project.create.sale.order'
partner_id = fields.Many2one('res.partner', string="Client", required=True, help="Client of the sales order")
def _make_billable_at_project_rate(self, sale_order):
self.ensure_one()
task_left = self.project_id.tasks.filtered(lambda task: not task.sale_line_id)
ticket_timesheet_ids = self.env.context.get('ticket_timesheet_ids', [])
budgeted_qty = 0
for wizard_line in self.line_ids:
task_ids = self.project_id.tasks.filtered(lambda task: not task.sale_line_id and task.timesheet_product_id == wizard_line.product_id)
task_left -= task_ids
# trying to simulate the SO line created a task, according to the product configuration
# To avoid, generating a task when confirming the SO
task_id = False
if task_ids and wizard_line.product_id.service_tracking in ['task_in_project', 'task_global_project']:
task_id = task_ids.ids[0]
# create SO line
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'product_id': wizard_line.product_id.id,
'price_unit': wizard_line.price_unit,
'project_id': self.project_id.id, # prevent to re-create a project on confirmation
'task_id': task_id,
'product_uom_qty': wizard_line.budgeted_qty,
})
budgeted_qty += wizard_line.budgeted_qty
if ticket_timesheet_ids and not self.project_id.sale_line_id and not task_ids:
# With pricing = "project rate" in project. When the user wants to create a sale order from a ticket in helpdesk
# The project cannot contain any tasks. Thus, we need to give the first sale_order_line created to link
# the timesheet to this first sale order line.
# link the project to the SO line
self.project_id.write({
'sale_order_id': sale_order.id,
'sale_line_id': sale_order_line.id,
'partner_id': self.partner_id.id,
})
# link the tasks to the SO line
task_ids.write({
'sale_line_id': sale_order_line.id,
'partner_id': sale_order.partner_id.id,
'email_from': sale_order.partner_id.email,
})
# assign SOL to timesheets
search_domain = [('task_id', 'in', task_ids.ids), ('so_line', '=', False)]
if ticket_timesheet_ids:
search_domain = [('id', 'in', ticket_timesheet_ids), ('so_line', '=', False)]
self.env['account.analytic.line'].search(search_domain).write({
'so_line': sale_order_line.id
})
#sale_order_line.with_context({'no_update_planned_hours': True}).write({
#'product_uom_qty': sale_order_line.qty_delivered
#})
if ticket_timesheet_ids and self.project_id.sale_line_id and not self.project_id.tasks and len(self.line_ids) > 1:
# Then, we need to give to the project the last sale order line created
self.project_id.write({
'sale_line_id': sale_order_line.id
})
else: # Otherwise, we are in the normal behaviour
# link the project to the SO line
self.project_id.write({
'sale_order_id': sale_order.id,
'sale_line_id': sale_order_line.id, # we take the last sale_order_line created
'partner_id': self.partner_id.id,
})
if self.project_id.budgeted_hours <= 0:
self.project_id.write({
'budgeted_hours': budgeted_qty,
})
if task_left:
task_left.sale_line_id = False
def _make_billable_at_employee_rate(self, sale_order):
# trying to simulate the SO line created a task, according to the product configuration
# To avoid, generating a task when confirming the SO
task_id = self.env['project.task'].search([('project_id', '=', self.project_id.id)], order='create_date DESC', limit=1).id
project_id = self.project_id.id
lines_already_present = dict([(l.employee_id.id, l) for l in self.project_id.sale_line_employee_ids])
non_billable_tasks = self.project_id.tasks.filtered(lambda task: not task.sale_line_id)
non_allow_billable_tasks = self.project_id.tasks.filtered(lambda task: task.non_allow_billable)
map_entries = self.env['project.sale.line.employee.map']
EmployeeMap = self.env['project.sale.line.employee.map'].sudo()
# create SO lines: create on SOL per product/price. So many employee can be linked to the same SOL
map_product_price_sol = {} # (product_id, price) --> SOL
budgeted_qty = 0
for wizard_line in self.line_ids:
map_key = (wizard_line.product_id.id, wizard_line.price_unit)
if map_key not in map_product_price_sol:
values = {
'order_id': sale_order.id,
'product_id': wizard_line.product_id.id,
'price_unit': wizard_line.price_unit,
'product_uom_qty': wizard_line.budgeted_qty,
}
budgeted_qty += wizard_line.budgeted_qty
if wizard_line.product_id.service_tracking in ['task_in_project', 'task_global_project']:
values['task_id'] = task_id
if wizard_line.product_id.service_tracking in ['task_in_project', 'project_only']:
values['project_id'] = project_id
sale_order_line = self.env['sale.order.line'].create(values)
map_product_price_sol[map_key] = sale_order_line
if wizard_line.employee_id.id not in lines_already_present:
map_entries |= EmployeeMap.create({
'project_id': self.project_id.id,
'sale_line_id': map_product_price_sol[map_key].id,
'employee_id': wizard_line.employee_id.id,
})
else:
map_entries |= lines_already_present[wizard_line.employee_id.id]
lines_already_present[wizard_line.employee_id.id].write({
'sale_line_id': map_product_price_sol[map_key].id
})
# link the project to the SO
self.project_id.write({
'sale_order_id': sale_order.id,
'sale_line_id': sale_order.order_line[0].id,
'partner_id': self.partner_id.id,
})
if self.project_id.budgeted_hours <= 0:
self.project_id.write({
'budgeted_hours': budgeted_qty,
})
non_billable_tasks.write({
'partner_id': sale_order.partner_id.id,
'email_from': sale_order.partner_id.email,
})
non_allow_billable_tasks.sale_line_id = False
tasks = self.project_id.tasks.filtered(lambda t: not t.non_allow_billable)
# assign SOL to timesheets
for map_entry in map_entries:
search_domain = [('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]
ticket_timesheet_ids = self.env.context.get('ticket_timesheet_ids', [])
if ticket_timesheet_ids:
search_domain.append(('id', 'in', ticket_timesheet_ids))
else:
search_domain.append(('task_id', 'in', tasks.ids))
self.env['account.analytic.line'].search(search_domain).write({
'so_line': map_entry.sale_line_id.id
})
#map_entry.sale_line_id.with_context({'no_update_planned_hours': True}).write({
# 'product_uom_qty': map_entry.sale_line_id.qty_delivered
#})
return map_entries
class ProjectCreateSalesOrderLine(models.TransientModel):
_inherit= 'project.create.sale.order.line'
employee_id = fields.Many2one('hr.employee', string="Consultant", help="Consultant that has timesheets on the project.")
budgeted_qty = fields.Float(string='Budgeted Hour', digits='Product Unit of Measure', default=1.0)
budgeted_uom = fields.Many2one('uom.uom', string='Budgeted UOM', related='product_id.uom_id', readonly=True)
employee_price = fields.Float("Consultant Price")
price_unit = fields.Float("Hourly rate", help="Unit price of the sales order item.")

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_create_sale_order_inherit1_view_form" model="ir.ui.view">
<field name="name">project.create.sale.order.wizard.form.budgeted</field>
<field name="model">project.create.sale.order</field>
<field name="inherit_id" ref="sale_timesheet.project_create_sale_order_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='line_ids']//tree//field[@name='product_id']" position="after">
<field name="budgeted_qty" string="Budgeted Hour"/> <!-- attrs="{'column_invisible': [('parent.pricing_type','=','fixed_rate')]}" -->
<!--<field name="budgeted_uom" attrs="{'column_invisible': [('parent.pricing_type','=','fixed_rate')]}"/>-->
</xpath>
</field>
</record>
</odoo>

103
mail_debrand/README.rst Executable file
View File

@ -0,0 +1,103 @@
============
Mail Debrand
============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Production/Stable
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github
:target: https://github.com/OCA/social/tree/13.0/mail_debrand
:alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/social-13-0/social-13-0-mail_debrand
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/205/13.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module modifies the functionality of emails to remove the Odoo branding,
specifically the 'using Odoo' of notifications or the 'Powered by Odoo'
**Table of contents**
.. contents::
:local:
Usage
=====
To use this module, you need to:
* Install it.
* Send an email.
* Nobody will know it comes from Odoo.
Changelog
=========
12.0.1.0.0 (2018-11-06)
~~~~~~~~~~~~~~~~~~~~~~~
* [NEW] Initial V12 version. Complete rewrite from v11.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/social/issues/new?body=module:%20mail_debrand%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
* Eficent
* Onestein
Contributors
~~~~~~~~~~~~
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
* Lois Rilo <lois.rilo@eficent.com>
* Graeme Gellatly <graeme@o4sb.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px
:target: https://github.com/pedrobaeza
:alt: pedrobaeza
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-pedrobaeza|
This module is part of the `OCA/social <https://github.com/OCA/social/tree/13.0/mail_debrand>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

1
mail_debrand/__init__.py Executable file
View File

@ -0,0 +1 @@
from . import models

19
mail_debrand/__manifest__.py Executable file
View File

@ -0,0 +1,19 @@
# Copyright 2016 Tecnativa - Jairo Llopis
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2019 Eficent Business and IT Consulting Services S.L.
# - Lois Rilo <lois.rilo@eficent.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Mail Debrand",
"summary": "Remove Odoo branding in sent emails",
"version": "13.0.2.0.1",
"category": "Social Network",
"website": "https://github.com/OCA/social/",
"author": "Tecnativa, Eficent, Onestein, Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": ["mail"],
"development_status": "Production/Stable",
"maintainers": ["pedrobaeza"],
}

45
mail_debrand/i18n/fr.po Executable file
View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2020-08-12 17:59+0000\n"
"Last-Translator: Weblate Admin <weblate@odoo-community.org>\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 3.10\n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr "Modèles d'emails"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr "Discussion par email"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr "Odoo"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr "Propulsé par"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr "utilisant"

View File

@ -0,0 +1,42 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr ""
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr ""

45
mail_debrand/i18n/nl.po Executable file
View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2020-04-30 11:19+0000\n"
"Last-Translator: Raf Ven <raf.ven@dynapps.be>\n"
"Language-Team: none\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.10\n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr ""
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr "Aangeboden door"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr ""

45
mail_debrand/i18n/pt.po Executable file
View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2020-03-02 17:13+0000\n"
"Last-Translator: Pedro Castro Silva <pedrocs@exo.pt>\n"
"Language-Team: none\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 3.10\n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr "Modelos de Email"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr "Thread do email"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr ""

46
mail_debrand/i18n/sl.po Executable file
View File

@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2020-03-19 10:13+0000\n"
"Last-Translator: Matjaz Mozetic <matjaz@matmoz.si>\n"
"Language-Team: none\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
"%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 3.10\n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr "Predloge e-pošte"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr "E-poštna nit"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr "Odoo"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr "Powered by"
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr "z uporabo"

44
mail_debrand/i18n/sr_Latn.po Executable file
View File

@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_debrand
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: sr_Latn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_template
msgid "Email Templates"
msgstr ""
#. module: mail_debrand
#: model:ir.model,name:mail_debrand.model_mail_thread
msgid "Email Thread"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Odoo"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "Powered by"
msgstr ""
#. module: mail_debrand
#: code:addons/mail_debrand/models/mail_template.py:0
#, python-format
msgid "using"
msgstr ""

View File

@ -0,0 +1,2 @@
from . import mail_template
from . import mail_thread

View File

@ -0,0 +1,51 @@
# Copyright 2019 O4SB - Graeme Gellatly
# Copyright 2019 Tecnativa - Ernesto Tejeda
# Copyright 2020 Onestein - Andrea Stirpe
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import re
from lxml import html as htmltree
from odoo import _, api, models
class MailTemplate(models.Model):
_inherit = "mail.template"
@api.model
def _debrand_translated_words(self):
def _get_translated(word):
return self.env["ir.translation"]._get_source(
"ir.ui.view,arch_db", "model_terms", self.env.lang, word
)
odoo_word = _get_translated("Odoo") or _("Odoo")
powered_by = _get_translated("Powered by") or _("Powered by")
using_word = _get_translated("using") or _("using")
return odoo_word, powered_by, using_word
@api.model
def _debrand_body(self, html):
odoo_word, powered_by, using_word = self._debrand_translated_words()
html = re.sub(using_word + "(.*)[\r\n]*(.*)>" + odoo_word + r"</a>", "", html)
if powered_by not in html:
return html
root = htmltree.fromstring(html)
powered_by_elements = root.xpath("//*[text()[contains(.,'%s')]]" % powered_by)
for elem in powered_by_elements:
# make sure it isn't a spurious powered by
if any(
[
"www.odoo.com" in child.get("href", "")
for child in elem.getchildren()
]
):
for child in elem.getchildren():
elem.remove(child)
elem.text = None
return htmltree.tostring(root).decode("utf-8")
@api.model
def render_post_process(self, html):
html = super().render_post_process(html)
return self._debrand_body(html)

View File

@ -0,0 +1,14 @@
# Copyright 2019 Eficent Business and IT Consulting Services S.L.
# Lois Rilo <lois.rilo@eficent.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
def _replace_local_links(self, html, base_url=None):
html = super()._replace_local_links(html, base_url=base_url)
html_debranded = self.env["mail.template"]._debrand_body(html)
return html_debranded

View File

@ -0,0 +1,3 @@
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
* Lois Rilo <lois.rilo@eficent.com>
* Graeme Gellatly <graeme@o4sb.com>

View File

@ -0,0 +1,2 @@
This module modifies the functionality of emails to remove the Odoo branding,
specifically the 'using Odoo' of notifications or the 'Powered by Odoo'

View File

@ -0,0 +1,4 @@
12.0.1.0.0 (2018-11-06)
~~~~~~~~~~~~~~~~~~~~~~~
* [NEW] Initial V12 version. Complete rewrite from v11.

5
mail_debrand/readme/USAGE.rst Executable file
View File

@ -0,0 +1,5 @@
To use this module, you need to:
* Install it.
* Send an email.
* Nobody will know it comes from Odoo.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,248 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 128 128"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="icon.svg"
width="128"
height="128"
inkscape:export-filename="icon.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata128">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1855"
inkscape:window-height="1176"
id="namedview126"
showgrid="false"
inkscape:zoom="1.4583592"
inkscape:cx="-40.114514"
inkscape:cy="5.0469316"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<linearGradient
id="linearGradient5060">
<stop
offset="0"
id="stop7" />
<stop
offset="1"
stop-opacity="0"
id="stop9" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id="linearGradient5027"
x2="0"
y1="-150.69685"
y2="327.6604">
<stop
offset="0"
stop-opacity="0"
id="stop12" />
<stop
offset=".5"
id="stop14" />
<stop
offset="1"
stop-opacity="0"
id="stop16" />
</linearGradient>
<linearGradient
gradientTransform="matrix(2.9186598,0,0,2.6345737,-4.8428991,-17.303404)"
gradientUnits="userSpaceOnUse"
id="linearGradient6738"
x1="16.25"
x2="31.5"
y1="12.25"
y2="36.625">
<stop
offset="0"
stop-color="#fff"
id="stop19" />
<stop
offset="1"
stop-color="#ddd"
id="stop21" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id="linearGradient6954"
x2="0"
y1="29.22382"
y2="35.47382">
<stop
offset="0"
stop-color="#fff"
id="stop24" />
<stop
offset="1"
stop-color="#c9c9c9"
id="stop26" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id="linearGradient6956"
x1="26.504271"
x2="28.364229"
xlink:href="#linearGradient5060"
y1="35.819832"
y2="36.569832"
gradientTransform="translate(5.9124945,0.99230678)" />
<linearGradient
gradientTransform="matrix(-0.97769,0.21008,0.21008,0.97769,61.56807,-4.44833)"
gradientUnits="userSpaceOnUse"
id="linearGradient6958"
x1="22.686769"
x2="21.408461"
xlink:href="#linearGradient5060"
y1="36.3904"
y2="35.739632" />
<radialGradient
cx="55"
cy="125"
gradientUnits="userSpaceOnUse"
id="radialGradient278"
r="14.375">
<stop
offset="0"
stop-color="#fff"
id="stop31" />
<stop
offset=".5"
stop-color="#fff520"
stop-opacity=".89109"
id="stop33" />
<stop
offset="1"
stop-color="#fff300"
stop-opacity="0"
id="stop35" />
</radialGradient>
<radialGradient
cx="16.214741"
cy="19.83647"
gradientTransform="matrix(1,0,0,0.68192,19.76231,14.9041)"
gradientUnits="userSpaceOnUse"
id="radialGradient6948"
r="13.56536">
<stop
offset="0"
stop-color="#727e0a"
id="stop38" />
<stop
offset="1"
stop-color="#5b6508"
id="stop40" />
</radialGradient>
<radialGradient
cx="29.344931"
cy="17.064079"
gradientTransform="matrix(0.788,0,0,0.788,6.2212,3.61763)"
gradientUnits="userSpaceOnUse"
id="radialGradient6952"
r="9.1620598">
<stop
offset="0"
stop-color="#e9b15e"
id="stop43" />
<stop
offset="1"
stop-color="#966416"
id="stop45" />
</radialGradient>
<radialGradient
cx="605.71429"
cy="486.64789"
gradientTransform="matrix(-2.77439,0,0,1.96971,112.7623,-872.8854)"
gradientUnits="userSpaceOnUse"
id="radialGradient5031"
r="117.14286"
xlink:href="#linearGradient5060" />
<radialGradient
cx="605.71429"
cy="486.64789"
gradientTransform="matrix(2.77439,0,0,1.96971,-1891.633,-872.8854)"
gradientUnits="userSpaceOnUse"
id="radialGradient5029"
r="117.14286"
xlink:href="#linearGradient5060" />
<radialGradient
cx="31.1127"
cy="19.008619"
gradientUnits="userSpaceOnUse"
id="radialGradient6950"
r="8.6620598"
xlink:href="#linearGradient5060" />
</defs>
<g
transform="matrix(2.1385661,0,0,2.1385661,-36.355623,89.820203)"
inkscape:label="Calque 1"
id="layer1">
<path
transform="translate(13,-50)"
d="m 10,8 c -3.3137,0 -6,2.6822 -6,6 0.074392,9.0983 1.952e-4,18.727 0,28 0,3.1932 2.4839,5.7776 5.625,5.9688 l 44.625,0.031 c 3.199,-0.129 5.75,-2.765 5.75,-6 l 0,-29.031 c -0.485,-2.829 -2.941,-4.969 -5.906,-4.969 -14.69,0.0067 -29.398,1e-4 -44.094,0 z m -0.09375,3.1562 44.375,0 -21.469,21.469 c -0.39174,0.39174 -1.0145,0.39174 -1.4062,0 l -21.5,-21.469 z m 47.094,1.532 0,29.093 c 0,1.7334 -0.6245,2.7125 -1.875,3.0625 l -15.125,-15.156 17,-17 z m -49.656,0.124 16.718,16.719 -15.281,15.281 c -1.046,-0.353 -1.4101,-1.195 -1.4372,-2.343 l 0,-29.656 z m 18.844,18.812 5.2188,5.25 c 0.39174,0.39174 1.0145,0.39174 1.4062,0 l 5.062,-5.062 13.187,13.188 -38.25,0 13.375,-13.375 z"
sodipodi:nodetypes="ccscccccccccccccccccccccccccccc"
style="fill:#000000"
id="rect3174"
inkscape:connector-curvature="0" />
</g>
<g
transform="matrix(0.26759984,0,0,-0.26759984,62.366861,118.02764)"
inkscape:label="ink_ext_XXXXXX"
id="g10">
<path
d="m 104.00014,165.45424 c -8.313043,0 -16.263246,-1.57861 -23.850744,-4.72609 -7.594015,-3.15718 -14.181554,-7.52151 -19.768717,-13.10966 -5.587026,-5.58537 -10.031894,-12.10596 -13.321854,-19.55278 -3.297028,-7.45376 -4.942836,-15.61837 -4.942836,-24.4966 0,-8.312905 1.645808,-16.26269 4.942836,-23.850746 3.28996,-7.594012 7.734828,-14.256255 13.321854,-19.984093 5.587163,-5.733797 12.174702,-10.246299 19.768717,-13.536809 7.587498,-3.297027 15.537701,-4.941729 23.850744,-4.941729 8.30583,0 16.2577,1.644702 23.85171,4.941729 7.5875,3.29051 14.18128,7.803012 19.76942,13.536809 5.58675,5.727838 10.02454,12.390081 13.32171,19.984093 3.29023,7.588056 4.94089,15.537841 4.94089,23.850746 -0.85928,16.90258 -6.87567,31.15898 -18.04779,42.76184 -11.17419,11.60314 -25.6454,17.97571 -43.40561,19.12329 l -0.43033,0 z M 104.43047,208 c 14.03437,0 27.28859,-2.79405 39.75226,-8.38083 12.46244,-5.58534 23.4211,-13.10687 32.87602,-22.5618 9.45355,-9.45492 16.97508,-20.48705 22.56183,-33.09085 C 205.20733,131.3558 208,117.89258 208,103.56911 208,89.239815 205.27385,75.85019 199.83679,63.387487 194.39,50.925336 187.01124,39.966662 177.70323,30.511325 168.38966,21.056678 157.431,13.602838 144.82719,8.1646355 132.21703,2.726419 118.61203,0 104.00014,0 89.671258,0 76.208041,2.5780813 63.603558,7.7347705 50.993109,12.892 39.96777,20.123798 30.512431,29.437215 21.058202,38.744259 13.605015,49.702934 8.1651761,62.313379 2.7204595,74.917448 0,88.668665 0,103.56911 c 0,14.32347 2.6462976,27.71892 7.9502436,40.1826 5.2990814,12.46381 12.6781754,23.34765 22.1335154,32.66119 9.453538,9.30801 20.553026,16.76025 33.305391,22.347 12.744188,5.58675 26.429298,8.66218 41.04132,9.2401"
style="fill:#a2478a;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path14"
inkscape:connector-curvature="0" />
</g>
<g
id="g4250"
transform="matrix(0.16801262,0,0,0.16801262,85.012954,66.157096)">
<ellipse
style="fill:none;stroke:#ff0000;stroke-width:50.00000381"
id="path3315"
cx="30.856556"
cy="143.08545"
rx="199.9995"
ry="199.99942" />
<path
d="m -114.84995,20.228255 297.14,260.000005"
inkscape:connector-curvature="0"
style="fill:#ff0000;stroke:#ff0000;stroke-width:50"
id="path4089" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,449 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Mail Debrand</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="mail-debrand">
<h1 class="title">Mail Debrand</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/social/tree/13.0/mail_debrand"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/social-13-0/social-13-0-mail_debrand"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/205/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module modifies the functionality of emails to remove the Odoo branding,
specifically the using Odoo of notifications or the Powered by Odoo</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="id3">Changelog</a><ul>
<li><a class="reference internal" href="#id1" id="id4">12.0.1.0.0 (2018-11-06)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<p>To use this module, you need to:</p>
<ul class="simple">
<li>Install it.</li>
<li>Send an email.</li>
<li>Nobody will know it comes from Odoo.</li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#id3">Changelog</a></h1>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id4">12.0.1.0.0 (2018-11-06)</a></h2>
<ul class="simple">
<li>[NEW] Initial V12 version. Complete rewrite from v11.</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id5">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mail_debrand%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
<li>Eficent</li>
<li>Onestein</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id8">Contributors</a></h2>
<ul class="simple">
<li>Pedro M. Baeza &lt;<a class="reference external" href="mailto:pedro.baeza&#64;tecnativa.com">pedro.baeza&#64;tecnativa.com</a>&gt;</li>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;eficent.com">lois.rilo&#64;eficent.com</a>&gt;</li>
<li>Graeme Gellatly &lt;<a class="reference external" href="mailto:graeme&#64;o4sb.com">graeme&#64;o4sb.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/pedrobaeza"><img alt="pedrobaeza" src="https://github.com/pedrobaeza.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/13.0/mail_debrand">OCA/social</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

1
mail_debrand/tests/__init__.py Executable file
View File

@ -0,0 +1 @@
from . import test_mail_debrand

View File

@ -0,0 +1,38 @@
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2020 Onestein - Andrea Stirpe
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
from odoo.tools.misc import mute_logger
class TestMailDebrand(common.TransactionCase):
def setUp(self):
super().setUp()
self.default_arch = self.env.ref("mail.message_notification_email").arch
self.paynow_arch = self.env.ref("mail.mail_notification_paynow").arch
def test_default_debrand(self):
self.assertIn("using", self.default_arch)
res = self.env["mail.template"]._debrand_body(self.default_arch)
self.assertNotIn("using", res)
def test_paynow_debrand(self):
self.assertIn("Powered by", self.paynow_arch)
res = self.env["mail.template"]._debrand_body(self.paynow_arch)
self.assertNotIn("Powered by", res)
def test_lang_paynow_debrand(self):
with mute_logger("odoo.addons.base.models.ir_translation"):
self.env["base.language.install"].create(
{"lang": "nl_NL", "overwrite": True}
).lang_install()
with mute_logger("odoo.tools.translate"):
self.env["base.update.translations"].create({"lang": "nl_NL"}).act_update()
ctx = dict(lang="nl_NL")
paynow_template = self.env.ref("mail.mail_notification_paynow")
paynow_arch = paynow_template.with_context(ctx).arch
self.assertIn("Aangeboden door", paynow_arch)
res = self.env["mail.template"].with_context(ctx)._debrand_body(paynow_arch)
self.assertNotIn("Aangeboden door", res)

1
odoo-debranding/__init__.py Executable file
View File

@ -0,0 +1 @@
from . import controllers

17
odoo-debranding/__manifest__.py Executable file
View File

@ -0,0 +1,17 @@
{
'name': 'odoo debranding',
'version': '14.0',
'summary': 'odoo debranding',
'description': 'odoo debranding',
'category': '',
'author': '',
'website': '',
'license': '',
'depends': ['base_setup', 'web'],
'data': ['views/webclient_templates.xml'],
'demo': [''],
'qweb': ["static/src/xml/base.xml"],
'installable': True,
'auto_install': False,
}

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import main

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import jinja2
import json
import odoo
from odoo import http
from odoo.addons.web.controllers.main import DBNAME_PATTERN, db_monodb,\
Database as DB
loader = jinja2.PackageLoader('odoo.addons.odoo-debranding',
"views")
env = jinja2.Environment(loader=loader, autoescape=True)
env.filters["json"] = json.dumps
class Database(DB):
def _render_template(self, **d):
d.setdefault('manage', True)
d['insecure'] = odoo.tools.config['admin_passwd'] == 'admin'
d['list_db'] = odoo.tools.config['list_db']
d['langs'] = odoo.service.db.exp_list_lang()
d['countries'] = odoo.service.db.exp_list_countries()
d['pattern'] = DBNAME_PATTERN
# databases list
d['databases'] = []
try:
d['databases'] = http.db_list()
except odoo.exceptions.AccessDenied:
monodb = db_monodb()
if monodb:
d['databases'] = [monodb]
return env.get_template("database_manager.html").render(d)

View File

@ -0,0 +1 @@
from . import odoodebrand

View File

@ -0,0 +1,17 @@
from odoo import models, fields, api, _
from odoo.exceptions import Warning
class WebsiteConfig(models.Model):
_inherit = 'res.config.settings'
company_logo = fields.Binary(related='website_id.company_logo',
string="Company Logo",
help="This field holds the image"
" used for the Company Logo",
readonly=False)
company_name = fields.Char(related='website_id.company_name',
string="Company Name",
readonly=False)
company_website = fields.Char(related='website_id.company_website',
readonly=False)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="mail.client_action">
<t t-jquery=".o_mail_request_permission" t-operation="inner">
Your permission is required to <a href="#"> enable desktop notifications</a>.
</t>
</t>
<t t-extend="UserMenu.Actions">
<t t-jquery="a[data-menu='documentation']" t-operation="replace">
</t>
<t t-jquery="a[data-menu='support']" t-operation="replace">
</t>
<!-- <t t-jquery="a[data-menu='about']" t-operation="replace"></t> -->
<t t-jquery="a[data-menu='account']" t-operation="replace">
</t>
<t t-jquery="li.divider" t-operation="replace">
</t>
</t>
<t t-extend="res_config_edition">
<t t-jquery="div[id='edition']" t-operation="replace">
</t>
</t>
<t t-extend="res_config_dev_tool">
<t t-jquery="div[id='developer_tool']" t-operation="replace"/>
</t>
</templates>

View File

@ -0,0 +1,422 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Odoo</title>
<link rel="shortcut icon" href="/odoo-debranding/static/src/img/image.png" type="image/x-icon">
<link rel="stylesheet" href="/web/static/lib/fontawesome/css/font-awesome.css">
<link rel="stylesheet" href="/web/static/lib/bootstrap/css/bootstrap.css">
<script src="/web/static/lib/jquery/jquery.js" type="text/javascript"></script>
<script type="text/javascript" src="/web/static/lib/popper/popper.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/index.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/util.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/alert.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/button.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/carousel.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/collapse.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dropdown.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/modal.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/tooltip.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/popover.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/scrollspy.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/tab.js"></script>
<script type="text/javascript">
$(function() {
// Little eye
$('body').on('mousedown', '.o_little_eye', function (ev) {
$(ev.target).closest('.input-group').find('.form-control').prop("type",
(i, old) => { return old === "text" ? "password" : "text"; }
);
});
// db modal
$('body').on('click', '.o_database_action', function (ev) {
ev.preventDefault();
var db = $(ev.currentTarget).data('db');
var target = $(ev.currentTarget).data('target');
$(target).find('input[name=name]').val(db);
$(target).modal();
});
// close modal on submit
$('.modal').on('submit', 'form', function (ev) {
var form = $(this).closest('form')[0];
if (form && form.checkValidity && !form.checkValidity()) {
return;
}
var modal = $(this).parentsUntil('body', '.modal');
if (modal.hasClass('o_database_backup')) {
$(modal).modal('hide');
if (!$('.alert-backup-long').length) {
$('.list-group').before("<div class='alert alert-info alert-backup-long'>The backup may take some time before being ready</div>");
}
}
});
// generate a random master password
// removed l1O0 to avoid confusions
var charset = "abcdefghijkmnpqrstuvwxyz23456789";
var password = "";
for (var i = 0, n = charset.length; i < 12; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
if (i === 3 || i === 7) {
password += "-";
}
}
var master_pwds = document.getElementsByClassName("generated_master_pwd");
for (var i=0, len=master_pwds.length|0; i<len; i=i+1|0) {
master_pwds[i].innerText = password;
}
var master_pwd_inputs = document.getElementsByClassName("generated_master_pwd_input");
for (var i=0, len=master_pwd_inputs.length|0; i<len; i=i+1|0) {
master_pwd_inputs[i].value = password;
master_pwd_inputs[i].setAttribute('autocomplete', 'new-password');
}
});
</script>
</head>
{% macro master_input(set_master_pwd=False) -%}
{% set input_class = "form-control" %}
{% if insecure %}
{% if set_master_pwd %}
<input type="hidden" name="master_pwd" class="form-control" value="admin"/>
{% else %}
<div class="alert alert-warning">
<p>Warning, your Odoo database manager is not protected. To secure it, we have generated the following master password for it:</p>
<p style="text-align: center;"><strong class="generated_master_pwd"></strong></p>
<p>You can change it below but be sure to remember it, it will be asked for future operations on databases.</p>
</div>
{% set input_class = "form-control generated_master_pwd_input" %}
{% endif %}
{% endif %}
{% if not insecure or not set_master_pwd %}
<div class="form-group row">
<label for="master_pwd" class="col-md-4 col-form-label">Master Password</label>
<div class="col-md-8 input-group">
<input name="master_pwd"
class="{{ input_class}}"
required="required"
autofocus="autofocus"
type="password"
autocomplete="current-password" />
<div class="input-group-append">
<span class="fa fa-eye o_little_eye input-group-text" aria-hidden="true" style="cursor: pointer;"></span>
</div>
</div>
</div>
{% endif %}
{%- endmacro %}
{% macro create_form() -%}
{{ master_input() }}
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label">Database Name</label>
<div class="col-md-8">
<input id="dbname" type="text" name="name" class="form-control" required="required" autocomplete="off" pattern="{{ pattern }}" title="Only alphanumerical characters, underscore, hyphen and dot are allowed"/>
</div>
</div>
<div class="form-group row">
<label for="login" class="col-md-4 col-form-label">Email</label>
<div class="col-md-8">
<input id="login" type="text" name="login" class="form-control" required="required" autocomplete="off"/>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label">Password</label>
<div class="col-md-8 input-group">
<input id="password" type="password" name="password" class="form-control" required="required" autocomplete="off"/>
<div class="input-group-append">
<span class="fa fa-eye o_little_eye input-group-text" aria-hidden="true" style="cursor: pointer;"></span>
</div>
</div>
</div>
<div class="form-group row">
<label for="phone" class="col-md-4 col-form-label">Phone number</label>
<div class="col-md-8 input-group">
<input id="phone" type="tel" name="phone" class="form-control" autocomplete="off"/>
</div>
</div>
<div class="form-group row">
<label for="lang" class="col-md-4 col-form-label">Language</label>
<div class="col-md-8">
<select id="lang" name="lang" class="form-control" required="required" autocomplete="off">
{% for lang in langs %}
<option {% if lang[0] == "en_US" %}selected="selected" {% endif %}value="{{ lang[0] }}">{{ lang[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="country" class="col-md-4 col-form-label ">Country</label>
<div class="col-md-8">
<select id="country" name="country_code" class="form-control" autocomplete="off">
<option value=""></option>
{% for country in countries %}
<option value="{{ country[0] }}">{{ country[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="demo" class="col-md-4 col-form-label">Demo data</label>
<div class="col-md-8">
<input type="checkbox" id="load_demo_checkbox" class="form-control-sm" name="demo" value="1">
</div>
</div>
{%- endmacro %}
<body>
<div class="container">
<!-- Database List -->
<div class="row">
<div class="col-lg-6 offset-lg-3 o_database_list">
<img src="/odoo-debranding/static/src/img/image.png" class="img-fluid d-block mx-auto"/>
{% if not list_db %}
<div class="alert alert-danger text-center">The database manager has been disabled by the administrator</div>
{% elif insecure and databases %}
<div class="alert alert-warning">
Warning, your Odoo database manager is not protected.<br/>
Please <a href="#" data-toggle="modal" data-target=".o_database_master">set a master password</a> to secure it.
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
{% if list_db and databases %}
<div class="list-group">
{% for db in databases %}
<div class="list-group-item d-flex align-items-center">
<a href="/web?db={{ db }}" class="d-block flex-grow-1">
{% if db in incompatible_databases %}
<i class="icon fa fa-warning float-right text-warning" title="This database may not be compatible"></i>
{% endif %}
{{ db }}
</a>
{% if manage %}
<div class="btn-group btn-group-sm float-right">
<button type="button" data-db="{{ db }}" data-target=".o_database_backup" class="o_database_action btn btn-primary">
<i class="fa fa-floppy-o fa-fw"></i> Backup
</button>
<button type="button" data-db="{{ db }}" data-target=".o_database_duplicate" class="o_database_action btn btn-secondary">
<i class="fa fa-files-o fa-fw"></i> Duplicate
</button>
<button type="button" data-db="{{ db }}" data-target=".o_database_delete" class="o_database_action btn btn-danger">
<i class="fa fa-trash-o fa-fw"></i> Delete
</button>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% if manage %}
<div class="d-flex mt-2">
<button type="button" data-toggle="modal" data-target=".o_database_create" class="btn btn-primary flex-grow-1">Create Database</button>
<button type="button" data-toggle="modal" data-target=".o_database_restore" class="btn btn-primary flex-grow-1 ml-2">Restore Database</button>
<button type="button" data-toggle="modal" data-target=".o_database_master" class="btn btn-primary flex-grow-1 ml-2">Set Master Password</button>
</div>
{% else %}
<div class="text-center mt-2">
<a href="/web/database/manager">Manage databases</a>
</div>
{% endif %}
{% elif list_db %}
<form role="form" action="/web/database/create" method="post">
{{ create_form() }}
<input type="submit" value="Create database" class="btn btn-primary float-left"/>
</form>
<a role="button" data-toggle="modal" data-target=".o_database_restore" class="btn btn-link">or restore a database</a>
{% endif %}
</div>
</div>
</div>
<!-- Create -->
<div class="modal fade o_database_create" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<form role="form" action="/web/database/create" method="post">
<div class="modal-header">
<h4 class="modal-title">Create Database</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{{ create_form() }}
<small class="text-muted">
To enhance your experience, some data may be sent to Odoo online services. See our <a href="https://www.odoo.com/privacy">Privacy Policy</a>.
</small>
</div>
<div class="modal-footer">
<input type="submit" value="Continue" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
<!-- Restore -->
<div class="modal fade o_database_restore" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Restore Database</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form id="form_restore_db" role="form" action="/web/database/restore" method="post" enctype="multipart/form-data">
<div class="modal-body">
{{ master_input() }}
<div class="form-group row">
<label for="backup_file" class="col-md-4 col-form-label">File</label>
<div class="col-md-8">
<input id="backup_file" type="file" name="backup_file" class="required"/>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label">Database Name</label>
<div class="col-md-8">
<input id="dbname_restore" type="text" name="name" class="form-control" required="required" pattern="{{ pattern }}" title="Only alphanumerical characters, underscore, hyphen and dot are allowed"/>
</div>
</div>
<div class="form-group">
<label for="copy">This database might have been moved or copied.</label>
<p class="form-text">
In order to avoid conflicts between databases, Odoo needs to know if this database was moved or copied.<br/>
If you don't know, answer "This database is a copy".
</p>
<div class="custom-control custom-radio">
<input id="radio_copy_true" name="copy" type="radio" class="custom-control-input" value="true" checked="1">
<label for="radio_copy_true" class="custom-control-label">This database is a copy</label>
</div>
<div class="custom-control custom-radio">
<input id="radio_copy_false" name="copy" type="radio" class="custom-control-input" value="false">
<label for="radio_copy_false" class="custom-control-label">This database was moved</label>
</div>
</div>
</div>
<div class="modal-footer">
<input type="submit" value="Continue" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
<!-- Master password -->
<div class="modal fade o_database_master" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Set Master Password</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form id="form_change_pwd" role="form" action="/web/database/change_password" method="post">
<div class="modal-body">
<p>The master password is required to create, delete, dump or restore databases.</p>
{{ master_input(set_master_pwd=True) }}
<div class="form-group">
<label for="master_pwd_new" class="col-form-label">New Master Password</label>
<div class="input-group">
<input id="master_pwd_new" type="password" name="master_pwd_new" class="form-control" required="required" autocomplete="new-password"/>
<div class="input-group-append">
<span class="fa fa-eye o_little_eye input-group-text" aria-hidden="true" style="cursor: pointer;"></span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<input type="submit" value="Continue" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
<!-- Duplicate DB -->
<div class="modal fade o_database_duplicate" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Duplicate Database</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form id="form-duplicate-db" role="form" action="/web/database/duplicate" method="post">
<div class="modal-body">
{{ master_input() }}
<div class="form-group">
<label for="name" class="col-form-label">Database Name</label>
<input id="dbname_duplicate" type="text" name="name" class="form-control" required="required" readonly="readonly"/>
</div>
<div class="form-group">
<label for="new_name" class="col-form-label">New Name</label>
<input id="new_name" type="text" name="new_name" class="form-control" required="required" pattern="{{ pattern }}" title="Only alphanumerical characters, underscore, hyphen and dot are allowed"/>
</div>
</div>
<div class="modal-footer">
<input type="submit" value="Continue" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
<!-- Drop DB -->
<div class="modal fade o_database_delete" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Delete Database</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form id="form_drop_db" role="form" action="/web/database/drop" method="post">
<div class="modal-body">
{{ master_input() }}
<div class="form-group">
<label for="name" class="col-form-label">Database</label>
<input id="dbname_delete" type="text" name="name" class="form-control" required="required" readonly="readonly"/>
</div>
</div>
<div class="modal-footer">
<input type="submit" value="Delete" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
<!-- Backup DB -->
<div class="modal fade o_database_backup" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Backup Database</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form id="form_backup_db" role="form" action="/web/database/backup" method="post">
<div class="modal-body">
{{ master_input() }}
<div class="form-group">
<label for="name" class="col-form-label">Database Name</label>
<input id="dbname_backup" type="text" name="name" class="form-control" required="required" readonly="readonly"/>
</div>
<div class="form-group">
<label for="backup_format" class="col-form-label">Backup Format</label>
<select id="backup_format" name="backup_format" class="form-control" required="required">
<option value="zip">zip (includes filestore)</option>
<option value="dump">pg_dump custom format (without filestore)</option>
</select>
</div>
</div>
<div class="modal-footer">
<input type="submit" value="Backup" class="btn btn-primary float-right"/>
</div>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,65 @@
<odoo>
<template id="web.layout" name="Web layout">&lt;!DOCTYPE html&gt;
<html t-att="html_data or {}">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<title t-esc="title or 'COR'"/>
<link type="image/x-icon" rel="shortcut icon" t-att-href="x_icon or '/odoo-debranding/static/src/img/image.png'"/>
<script type="text/javascript">
var odoo = {
csrf_token: "<t t-esc="request.csrf_token(None)"/>",
debug: "<t t-esc="debug"/>",
};
</script>
<t t-raw="head or ''"/>
</head>
<body t-att-class="body_classname">
<t t-raw="0"/>
</body>
</html>
</template>
<template id="web.login_layout" name="Login Layout">
<t t-call="web.frontend_layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="body_classname" t-value="'bg-100'"/>
<t t-set="no_header" t-value="True"/>
<t t-set="no_footer" t-value="True"/>
<div class="container py-5">
<div t-attf-class="card border-0 mx-auto bg-100 {{login_card_classes}} o_database_list" style="max-width: 300px;">
<div class="card-body">
<div t-attf-class="text-center pb-3 border-bottom {{'mb-3' if form_small else 'mb-4'}}">
<img t-attf-src="/web/binary/company_logo{{ '?dbname='+db if db else '' }}" alt="Logo" style="max-height:120px; max-width: 100%; width:auto"/>
</div>
<t t-raw="0"/>
<div class="text-center small mt-4 pt-3 border-top" t-if="not disable_footer">
<!--<t t-if="not disable_database_manager">
<a class="border-right pr-2 mr-1" href="/web/database/manager">Manage Databases</a>
</t>-->
<!-- <a href="https://www.odoo.com?utm_source=db&amp;utm_medium=auth" target="_blank">Powered by <span>Odoo</span></a>-->
</div>
</div>
</div>
</div>
</t>
</template>
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.remove.about</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">
<div id="about" position="replace"/>
</field>
</record>
</odoo>

7
planning/__init__.py Executable file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from . import wizard
from . import report

36
planning/__manifest__.py Executable file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Planning",
'summary': """Manage your employees' schedule""",
'description': """
Schedule your teams and employees with shift.
""",
'category': 'Human Resources/Planning',
'version': '1.0',
'depends': ['hr', 'web_gantt'],
'data': [
'security/planning_security.xml',
'security/ir.model.access.csv',
'wizard/planning_send_views.xml',
'views/assets.xml',
'views/hr_views.xml',
'report/planning_report_views.xml',
'views/planning_template_views.xml',
'views/planning_views.xml',
'views/res_config_settings_views.xml',
'views/planning_templates.xml',
'data/planning_cron.xml',
'data/mail_data.xml',
],
'demo': [
'data/planning_demo.xml',
],
'application': True,
'license': 'OEEL-1',
'qweb': [
'static/src/xml/planning_gantt.xml',
'static/src/xml/field_colorpicker.xml',
]
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

127
planning/controllers/main.py Executable file
View File

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licens
from odoo import http, fields, _
from odoo.http import request
from odoo.osv import expression
import pytz
from werkzeug.utils import redirect
import babel
from werkzeug.exceptions import Forbidden
from odoo import tools
class ShiftController(http.Controller):
@http.route(['/planning/<string:planning_token>/<string:employee_token>'], type='http', auth="public", website=True)
def planning(self, planning_token, employee_token, message=False, **kwargs):
""" Displays an employee's calendar and the current list of open shifts """
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', employee_token)], limit=1)
if not employee_sudo:
return request.not_found()
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', planning_token)], limit=1)
if not planning_sudo:
return request.not_found()
employee_tz = pytz.timezone(employee_sudo.tz or 'UTC')
employee_fullcalendar_data = []
open_slots = []
planning_slots = []
# domain of slots to display
domain = planning_sudo._get_domain_slots()[planning_sudo.id]
if planning_sudo.include_unassigned:
domain = expression.AND([domain, ['|', ('employee_id', '=', employee_sudo.id), ('employee_id', '=', False)]])
else:
domain = expression.AND([domain, [('employee_id', '=', employee_sudo.id)]])
planning_slots = request.env['planning.slot'].sudo().search(domain)
# filter and format slots
for slot in planning_slots:
if slot.employee_id:
employee_fullcalendar_data.append({
'title': '%s%s' % (slot.role_id.name, u' \U0001F4AC' if slot.name else ''),
'start': str(pytz.utc.localize(slot.start_datetime).astimezone(employee_tz).replace(tzinfo=None)),
'end': str(pytz.utc.localize(slot.end_datetime).astimezone(employee_tz).replace(tzinfo=None)),
'color': self._format_planning_shifts(slot.role_id.color),
'alloc_hours': slot.allocated_hours,
'slot_id': slot.id,
'note': slot.name,
'allow_self_unassign': slot.allow_self_unassign
})
else:
open_slots.append(slot)
return request.render('planning.period_report_template', {
'employee_slots_fullcalendar_data': employee_fullcalendar_data,
'open_slots_ids': open_slots,
'planning_slots_ids': planning_slots,
'planning_planning_id': planning_sudo,
'employee': employee_sudo,
'format_datetime': lambda dt, dt_format: tools.format_datetime(request.env, dt, dt_format=dt_format),
'message_slug': message,
})
@http.route('/planning/<string:token_planning>/<string:token_employee>/assign/<int:slot_id>', type="http", auth="public", methods=['post'], website=True)
def planning_self_assign(self, token_planning, token_employee, slot_id, **kwargs):
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
if not slot_sudo.exists():
return request.not_found()
if slot_sudo.employee_id:
raise Forbidden(_('You can not assign yourself to this shift.'))
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', token_employee)], limit=1)
if not employee_sudo:
return request.not_found()
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', token_planning)], limit=1)
if not planning_sudo or slot_sudo.id not in planning_sudo.slot_ids._ids:
return request.not_found()
slot_sudo.write({'employee_id': employee_sudo.id})
return redirect('/planning/%s/%s?message=%s' % (token_planning, token_employee, 'assign'))
@http.route('/planning/<string:token_planning>/<string:token_employee>/unassign/<int:shift_id>', type="http", auth="public", methods=['post'], website=True)
def planning_self_unassign(self, token_planning, token_employee, shift_id, **kwargs):
slot_sudo = request.env['planning.slot'].sudo().search([('id', '=', shift_id)], limit=1)
if not slot_sudo or not slot_sudo.allow_self_unassign:
return request.not_found()
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', token_employee)], limit=1)
if not employee_sudo or employee_sudo.id != slot_sudo.employee_id.id:
return request.not_found()
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', token_planning)], limit=1)
if not planning_sudo or slot_sudo.id not in planning_sudo.slot_ids._ids:
return request.not_found()
slot_sudo.write({'employee_id': False})
return redirect('/planning/%s/%s?message=%s' % (token_planning, token_employee, 'unassign'))
@staticmethod
def _format_planning_shifts(color_code):
"""Take a color code from Odoo's Kanban view and returns an hex code compatible with the fullcalendar library"""
switch_color = {
0: '#008784', # No color (doesn't work actually...)
1: '#EE4B39', # Red
2: '#F29648', # Orange
3: '#F4C609', # Yellow
4: '#55B7EA', # Light blue
5: '#71405B', # Dark purple
6: '#E86869', # Salmon pink
7: '#008784', # Medium blue
8: '#267283', # Dark blue
9: '#BF1255', # Fushia
10: '#2BAF73', # Green
11: '#8754B0' # Purple
}
return switch_color[color_code]

118
planning/data/mail_data.xml Executable file
View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="email_template_slot_single" model="mail.template">
<field name="name">Planning: new schedule (single shift)</field>
<field name="email_from">${(object.company_id.email or '')}</field>
<field name="subject">Planning: new schedule (single shift)</field>
<field name="email_to">${object.employee_id.work_email}</field>
<field name="model_id" ref="model_planning_slot"/>
<field name="auto_delete" eval="False"/>
<field name="body_html" type="html">
<div>
<p>Dear ${object.employee_id.name or ''},</p><br/>
<p>You have been assigned the following schedule:</p><br/>
<table style="table-layout: fixed; width: 80%; margin: auto;">
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">From</th>
<td style="padding: 5px;">${format_datetime(object.start_datetime, tz=object.employee_id.tz)}</td>
</tr>
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">To</th>
<td style="padding: 5px;">${format_datetime(object.end_datetime, tz=object.employee_id.tz)}</td>
</tr>
% if object.role_id
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">Role</th>
<td style="padding: 5px;">${object.role_id.name or ''}</td>
</tr>
% endif
% if object.project_id
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">Project</th>
<td style="padding: 5px;">${object.project_id.name or ''}</td>
</tr>
% endif
% if object.name
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">Note</th>
<td style="padding: 5px;">${object.name or ''}</td>
</tr>
% endif
</table>
<div style="text-align: center">
% if ctx.get('render_link')
<div style="display: inline-block; margin: 15px; text-align: center">
<a href="${ctx.unavailable_path}${object.employee_id.employee_token}" target="_blank"
style="padding: 5px 10px; color: #875A7B; text-decoration: none; background-color: #FFFFFF; border: 1px solid #FFFFFF; border-radius: 3px"
>I am unavailable</a>
</div>
% endif
% if ctx.get('render_link')
<div style="display: inline-block; margin: 15px; text-align: center">
<a href="/web?#action=${ctx.get('action_id')}&amp;model=planning.slot&amp;menu_id=${ctx.get('menu_id')}&amp;db=${'dbname' in ctx and ctx['dbname'] or '' }" target="_blank"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px"
>View Planning</a>
</div>
% endif
</div>
</div>
</field>
</record>
<record id="email_template_planning_planning" model="mail.template">
<field name="name">Planning: new schedule (multiple shifts)</field>
<field name="email_from">${(object.company_id.email or '')}</field>
<field name="subject">Your planning from ${format_datetime(object.start_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')} to ${format_datetime(object.end_datetime, tz=employee.tz if employee else 'UTC', dt_format='short')}</field>
<field name="email_to"></field><!-- Set in the code -->
<field name="model_id" ref="model_planning_planning"/>
<field name="auto_delete" eval="False"/><!-- TODO JEM change this as we are testing -->
<field name="body_html" type="html">
<div>
% if ctx.get('employee'):
<p>Dear ${ctx['employee'].name},</p>
% else:
<p>Hello,</p>
% endif
<p>
You have been assigned new shifts:
</p>
<table style="table-layout: fixed; width: 80%; margin: auto;">
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">From</th>
<td style="padding: 5px;">${format_datetime(object.start_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')}</td>
</tr>
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">To</th>
<td style="padding: 5px;">${format_datetime(object.end_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')}</td>
</tr>
% if object.project_id
<tr>
<th style="padding: 5px;text-align: left; width: 15%;">Project</th>
<td style="padding: 5px;">${object.project_id.name or ''}</td>
</tr>
% endif
</table>
% if ctx.get('planning_url'):
<div style="margin: 15px;">
<a href="${ctx.get('planning_url')}" target="_blank"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">View Your Planning</a>
</div>
% endif
% if ctx.get('slot_unassigned_count'):
<p>There are new open shifts available. Please assign yourself if you are available.</p>
% endif
% if ctx.get('message'):
<p>${ctx['message']}</p>
% endif
</div>
</field>
</record>
</data>
</odoo>

13
planning/data/planning_cron.xml Executable file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_forecast_schedule" model="ir.cron">
<field name="name">Planning: generate next recurring shifts</field>
<field name="model_id" ref="model_planning_recurrency"/>
<field name="state">code</field>
<field name="code">model._cron_schedule_next()</field>
<field name="interval_type">weeks</field>
<field name="numbercall">-1</field>
</record>
</data>
</odoo>

547
planning/data/planning_demo.xml Executable file
View File

@ -0,0 +1,547 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Project forecast will warn if the user assigned to a forecast doesn't have a timezone -->
<record id="base.user_demo" model="res.users">
<field name="tz">Europe/Brussels</field>
<field name="groups_id" eval="[(4,ref('planning.group_planning_user'))]"/>
</record>
<!-- Roles -->
<record id="planning_role_bartender" model="planning.role">
<field name="name">Bartender</field>
<field name="color">2</field>
</record>
<record id="planning_role_waiter" model="planning.role">
<field name="name">Waiter</field>
<field name="color">3</field>
</record>
<record id="planning_role_chef" model="planning.role">
<field name="name">Chef</field>
<field name="color">4</field>
</record>
<!-- Shift templates (morning shifts) -->
<record id="planning_template_chef_morning" model="planning.slot.template">
<field name="role_id" ref="planning_role_chef"/>
<field name="start_time" eval="8"/>
<field name="duration" eval="6"/>
</record>
<record id="planning_template_bartender_morning" model="planning.slot.template">
<field name="role_id" ref="planning_role_bartender"/>
<field name="start_time" eval="8"/>
<field name="duration" eval="8"/>
</record>
<record id="planning_template_waiter_morning" model="planning.slot.template">
<field name="role_id" ref="planning_role_waiter"/>
<field name="start_time" eval="8"/>
<field name="duration" eval="8"/>
</record>
<!-- Shift templates (evening shifts) -->
<record id="planning_template_chef_evening" model="planning.slot.template">
<field name="role_id" ref="planning_role_chef"/>
<field name="start_time" eval="14"/>
<field name="duration" eval="8"/>
</record>
<record id="planning_template_bartender_evening" model="planning.slot.template">
<field name="role_id" ref="planning_role_bartender"/>
<field name="start_time" eval="16"/>
<field name="duration" eval="8"/>
</record>
<record id="planning_template_waiter_evening" model="planning.slot.template">
<field name="role_id" ref="planning_role_waiter"/>
<field name="start_time" eval="16"/>
<field name="duration" eval="8"/>
</record>
<!-- Recurrencies -->
<record id="planning_recurrency_1" model="planning.recurrency">
<field name="repeat_interval" eval="1"/>
<field name="repeat_type">forever</field>
</record>
<record id="planning_recurrency_2" model="planning.recurrency">
<field name="repeat_interval" eval="1"/>
<field name="repeat_type">forever</field>
</record>
<record id="planning_recurrency_3" model="planning.recurrency">
<field name="repeat_interval" eval="1"/>
<field name="repeat_type">forever</field>
</record>
<!-- Week 1 -->
<!-- Monday morning shifts -->
<record id="planning_slot_111" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_112" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_113" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Monday evening shifts -->
<record id="planning_slot_114" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_115" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_116" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Tuesday morning shifts -->
<record id="planning_slot_121" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_122" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" eval="False"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="name" eval="'Check with Katy from the employment agency if they have someone to replace Randall this day'"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_123" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_jep"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Tuesday evening shifts -->
<record id="planning_slot_124" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_125" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_126" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Wednesday morning shifts -->
<record id="planning_slot_131" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_132" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_133" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Wednesday evening shifts -->
<record id="planning_slot_134" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_135" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="name" eval="'Mitchel is replacing Jennie until the end of the week'"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_136" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Thursday morning shifts -->
<record id="planning_slot_141" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_142" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_143" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Thursday evening shifts -->
<record id="planning_slot_144" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_145" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_146" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Friday morning shifts -->
<record id="planning_slot_151" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" eval="False"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_152" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_153" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Friday evening shifts -->
<record id="planning_slot_154" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_155" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_156" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Week 2 -->
<!-- Monday morning shifts -->
<record id="planning_slot_211" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_212" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_213" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Monday evening shifts -->
<record id="planning_slot_214" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_215" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_216" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Tuesday morning shifts -->
<record id="planning_slot_221" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_222" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_223" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_jep"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Tuesday evening shifts -->
<record id="planning_slot_224" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_225" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_226" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" eval="False"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Wednesday morning shifts -->
<record id="planning_slot_231" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_232" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_233" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Wednesday evening shifts -->
<record id="planning_slot_234" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_jth"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_235" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_236" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" eval="False"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Thursday morning shifts -->
<record id="planning_slot_241" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_242" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_243" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Thursday evening shifts -->
<record id="planning_slot_244" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="name" eval="'Ask Toni if she can show Mitchel how things are done in the kitchen because it is her first time working as a chef'"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_245" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_246" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Friday morning shifts -->
<record id="planning_slot_251" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="recurrency_id" ref="planning_recurrency_1"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_252" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 12:00:00')"/>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="recurrency_id" ref="planning_recurrency_2"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_253" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="employee_id" eval="False"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Friday evening shifts -->
<record id="planning_slot_254" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 20:00:00')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="role_id" ref="planning_role_chef"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_255" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_chs"/>
<field name="role_id" ref="planning_role_waiter"/>
<field name="recurrency_id" ref="planning_recurrency_3"/>
<field name="publication_warning" eval="False"/>
</record>
<record id="planning_slot_256" model="planning.slot">
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 22:00:00')"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="role_id" ref="planning_role_bartender"/>
<field name="publication_warning" eval="False"/>
</record>
<!-- Duplicate shifts that have a recurrence set -->
<function model="planning.recurrency" name="_repeat_slot" eval="[[ref('planning_recurrency_1'), ref('planning_recurrency_2'), ref('planning_recurrency_3')]]"/>
</data>
</odoo>

1175
planning/i18n/ar.po Executable file

File diff suppressed because it is too large Load Diff

1166
planning/i18n/az.po Executable file

File diff suppressed because it is too large Load Diff

1162
planning/i18n/az_AZ.po Executable file

File diff suppressed because it is too large Load Diff

1172
planning/i18n/bg.po Executable file

File diff suppressed because it is too large Load Diff

1212
planning/i18n/cs.po Executable file

File diff suppressed because it is too large Load Diff

1175
planning/i18n/da.po Executable file

File diff suppressed because it is too large Load Diff

1175
planning/i18n/de.po Executable file

File diff suppressed because it is too large Load Diff

1168
planning/i18n/el.po Executable file

File diff suppressed because it is too large Load Diff

1193
planning/i18n/es.po Executable file

File diff suppressed because it is too large Load Diff

1176
planning/i18n/fi.po Executable file

File diff suppressed because it is too large Load Diff

1288
planning/i18n/fr.po Executable file

File diff suppressed because it is too large Load Diff

1271
planning/i18n/he.po Executable file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More