diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..8a212d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# compiled python files +*.py[co] +__pycache__/ diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/board_user/__init__.py b/board_user/__init__.py new file mode 100755 index 0000000..5305644 --- /dev/null +++ b/board_user/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/board_user/__manifest__.py b/board_user/__manifest__.py new file mode 100755 index 0000000..0456606 --- /dev/null +++ b/board_user/__manifest__.py @@ -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'], +} \ No newline at end of file diff --git a/board_user/models/__init__.py b/board_user/models/__init__.py new file mode 100755 index 0000000..0f37786 --- /dev/null +++ b/board_user/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import board_users \ No newline at end of file diff --git a/board_user/models/board_users.py b/board_user/models/board_users.py new file mode 100755 index 0000000..adc6f2b --- /dev/null +++ b/board_user/models/board_users.py @@ -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 \ No newline at end of file diff --git a/board_user/security/board_users.xml b/board_user/security/board_users.xml new file mode 100755 index 0000000..c969722 --- /dev/null +++ b/board_user/security/board_users.xml @@ -0,0 +1,10 @@ + + + + All dashboards + the user will have an access to the the menu all dashboards + + + + + \ No newline at end of file diff --git a/board_user/security/ir.model.access.csv b/board_user/security/ir.model.access.csv new file mode 100755 index 0000000..c36e25d --- /dev/null +++ b/board_user/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/board_user/static/description/all_dashboards.gif b/board_user/static/description/all_dashboards.gif new file mode 100755 index 0000000..9651b04 Binary files /dev/null and b/board_user/static/description/all_dashboards.gif differ diff --git a/board_user/static/description/cover.gif b/board_user/static/description/cover.gif new file mode 100755 index 0000000..dcd63de Binary files /dev/null and b/board_user/static/description/cover.gif differ diff --git a/board_user/static/description/icon.png b/board_user/static/description/icon.png new file mode 100755 index 0000000..cf71a9e Binary files /dev/null and b/board_user/static/description/icon.png differ diff --git a/board_user/static/description/index.html b/board_user/static/description/index.html new file mode 100755 index 0000000..8fc903d --- /dev/null +++ b/board_user/static/description/index.html @@ -0,0 +1,25 @@ +
+
+

See all dashboards of each user

+

Get the list of users who sets up a dashboard.

+
+ +
+
+
+ +
+
+

Settings

+
+ +

+ Set which users can see others dashboards. + +

+
+
+ +
+
+
\ No newline at end of file diff --git a/board_user/static/description/settings.gif b/board_user/static/description/settings.gif new file mode 100755 index 0000000..3cad58b Binary files /dev/null and b/board_user/static/description/settings.gif differ diff --git a/board_user/static/src/js/abstract_controller_inherit.js b/board_user/static/src/js/abstract_controller_inherit.js new file mode 100755 index 0000000..2ea08c8 --- /dev/null +++ b/board_user/static/src/js/abstract_controller_inherit.js @@ -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 + }); + } + }, + +}); + +}); \ No newline at end of file diff --git a/board_user/static/src/js/board_view_inherit.js b/board_user/static/src/js/board_view_inherit.js new file mode 100755 index 0000000..75db0fe --- /dev/null +++ b/board_user/static/src/js/board_view_inherit.js @@ -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 '' 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 = $('
').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; + }, + }); + +}); \ No newline at end of file diff --git a/board_user/static/src/xml/board.xml b/board_user/static/src/xml/board.xml new file mode 100755 index 0000000..0f8f530 --- /dev/null +++ b/board_user/static/src/xml/board.xml @@ -0,0 +1,26 @@ + + diff --git a/board_user/views/assets_backend.xml b/board_user/views/assets_backend.xml new file mode 100755 index 0000000..564ecb9 --- /dev/null +++ b/board_user/views/assets_backend.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/board_user/views/board_users_view.xml b/board_user/views/board_users_view.xml new file mode 100755 index 0000000..c26f630 --- /dev/null +++ b/board_user/views/board_users_view.xml @@ -0,0 +1,28 @@ + + + + + + board_user list + board.users + + + + + + + + + + + + All dashboards per user + board.users + tree,form + + + + + + \ No newline at end of file diff --git a/cor_custom/__init__.py b/cor_custom/__init__.py new file mode 100755 index 0000000..a41b57f --- /dev/null +++ b/cor_custom/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models +from . import wizard +#from . import report \ No newline at end of file diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py new file mode 100755 index 0000000..5a94307 --- /dev/null +++ b/cor_custom/__manifest__.py @@ -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', + ], +} diff --git a/cor_custom/controllers/__init__.py b/cor_custom/controllers/__init__.py new file mode 100755 index 0000000..457bae2 --- /dev/null +++ b/cor_custom/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/cor_custom/controllers/controllers.py b/cor_custom/controllers/controllers.py new file mode 100755 index 0000000..a8a213a --- /dev/null +++ b/cor_custom/controllers/controllers.py @@ -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//', auth='public') +# def object(self, obj, **kw): +# return http.request.render('cor_custom.object', { +# 'object': obj +# }) diff --git a/cor_custom/demo/demo.xml b/cor_custom/demo/demo.xml new file mode 100755 index 0000000..1d8cbab --- /dev/null +++ b/cor_custom/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/cor_custom/i18n/he_IL.po b/cor_custom/i18n/he_IL.po new file mode 100755 index 0000000..524ce02 --- /dev/null +++ b/cor_custom/i18n/he_IL.po @@ -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 "Total" +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 "אינך יכול למלא ערך יותר משעות שתוקצבו" diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py new file mode 100755 index 0000000..1bcbe44 --- /dev/null +++ b/cor_custom/models/__init__.py @@ -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 diff --git a/cor_custom/models/analytic.py b/cor_custom/models/analytic.py new file mode 100755 index 0000000..cb15854 --- /dev/null +++ b/cor_custom/models/analytic.py @@ -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) \ No newline at end of file diff --git a/cor_custom/models/crm_lead.py b/cor_custom/models/crm_lead.py new file mode 100755 index 0000000..de4d749 --- /dev/null +++ b/cor_custom/models/crm_lead.py @@ -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""" diff --git a/cor_custom/models/hr_employee.py b/cor_custom/models/hr_employee.py new file mode 100755 index 0000000..3285c76 --- /dev/null +++ b/cor_custom/models/hr_employee.py @@ -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") diff --git a/cor_custom/models/models.py b/cor_custom/models/models.py new file mode 100755 index 0000000..c89ae76 --- /dev/null +++ b/cor_custom/models/models.py @@ -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 diff --git a/cor_custom/models/product.py b/cor_custom/models/product.py new file mode 100755 index 0000000..db51af5 --- /dev/null +++ b/cor_custom/models/product.py @@ -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" + diff --git a/cor_custom/models/project.py b/cor_custom/models/project.py new file mode 100755 index 0000000..6fe9cbd --- /dev/null +++ b/cor_custom/models/project.py @@ -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 diff --git a/cor_custom/models/project_overview.py b/cor_custom/models/project_overview.py new file mode 100755 index 0000000..82f0f9c --- /dev/null +++ b/cor_custom/models/project_overview.py @@ -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 + + + + diff --git a/cor_custom/models/sale.py b/cor_custom/models/sale.py new file mode 100755 index 0000000..d4e2c02 --- /dev/null +++ b/cor_custom/models/sale.py @@ -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)]",) diff --git a/cor_custom/report/__init__.py b/cor_custom/report/__init__.py new file mode 100755 index 0000000..22d6147 --- /dev/null +++ b/cor_custom/report/__init__.py @@ -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 diff --git a/cor_custom/report/project_profitability_report_analysis.py b/cor_custom/report/project_profitability_report_analysis.py new file mode 100755 index 0000000..dd7a013 --- /dev/null +++ b/cor_custom/report/project_profitability_report_analysis.py @@ -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) diff --git a/cor_custom/report/project_profitability_report_analysis_views.xml b/cor_custom/report/project_profitability_report_analysis_views.xml new file mode 100755 index 0000000..4624f49 --- /dev/null +++ b/cor_custom/report/project_profitability_report_analysis_views.xml @@ -0,0 +1,76 @@ + + + + + project.profitability.report.pivot + project.profitability.report + + + + + + + + + + + + + project.profitability.report.graph + project.profitability.report + + + + + + + + + + + + + + + + project.profitability.report.search + project.profitability.report + + + + + + + + + + + + + + + + + + + + + Project Costs and Revenues + project.profitability.report + pivot,graph + + { + 'group_by_no_leaf':1, + 'group_by':[], + 'sale_show_order_product_name': 1, + } + 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). + + + + + diff --git a/cor_custom/security/ir.model.access.csv b/cor_custom/security/ir.model.access.csv new file mode 100755 index 0000000..fdc1832 --- /dev/null +++ b/cor_custom/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/cor_custom/views/analytic_view.xml b/cor_custom/views/analytic_view.xml new file mode 100755 index 0000000..a03a447 --- /dev/null +++ b/cor_custom/views/analytic_view.xml @@ -0,0 +1,42 @@ + + + + + + + account.analytic.line.tree.hr_timesheet_inherit1 + account.analytic.line + + + + + + + + + + + + diff --git a/cor_custom/views/crm_view.xml b/cor_custom/views/crm_view.xml new file mode 100755 index 0000000..3f457ce --- /dev/null +++ b/cor_custom/views/crm_view.xml @@ -0,0 +1,88 @@ + + + + + crm.lead.cor.inherit1 + crm.lead + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + crm.lead.kanban.lead + crm.lead + + + + + + + + diff --git a/cor_custom/views/hr_employee_views.xml b/cor_custom/views/hr_employee_views.xml new file mode 100755 index 0000000..0c8f6e8 --- /dev/null +++ b/cor_custom/views/hr_employee_views.xml @@ -0,0 +1,32 @@ + + + + + hr.employee.form.inherit + hr.employee + + + + + + + + + + + diff --git a/cor_custom/views/hr_timesheet_templates.xml b/cor_custom/views/hr_timesheet_templates.xml new file mode 100755 index 0000000..f2eda16 --- /dev/null +++ b/cor_custom/views/hr_timesheet_templates.xml @@ -0,0 +1,38 @@ + + + + Timesheet Plan + qweb + project.project + + + + + + + + + + + ( %) + + + Total + + + + + Hourly Rate + + + + + + + + + diff --git a/cor_custom/views/menu_show_view.xml b/cor_custom/views/menu_show_view.xml new file mode 100755 index 0000000..a29e46c --- /dev/null +++ b/cor_custom/views/menu_show_view.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/cor_custom/views/project_view.xml b/cor_custom/views/project_view.xml new file mode 100755 index 0000000..5835b26 --- /dev/null +++ b/cor_custom/views/project_view.xml @@ -0,0 +1,139 @@ + + + + + Project Analytic + project.project + + +
+ +
+ + + + + + +
+
+ + + + project.project.form.inherit + project.project + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + + Projects + project.project + [] + tree,kanban,form + + + main + +

+ No projects found. Let's create one! +

+

+ Projects regroup tasks on the same topic and each have their own dashboard. +

+
+
+ + + + project.task.form.inherit + project.task + + + + + + + +
diff --git a/cor_custom/views/sale_views.xml b/cor_custom/views/sale_views.xml new file mode 100755 index 0000000..fe6604d --- /dev/null +++ b/cor_custom/views/sale_views.xml @@ -0,0 +1,51 @@ + + + + + sale.order.form.inherit + sale.order + + + + + + + + + + + + + + 1 + + + + + + + diff --git a/cor_custom/views/templates.xml b/cor_custom/views/templates.xml new file mode 100755 index 0000000..cea6b39 --- /dev/null +++ b/cor_custom/views/templates.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/cor_custom/views/views.xml b/cor_custom/views/views.xml new file mode 100755 index 0000000..de75043 --- /dev/null +++ b/cor_custom/views/views.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cor_custom/wizard/__init__.py b/cor_custom/wizard/__init__.py new file mode 100755 index 0000000..169f7b9 --- /dev/null +++ b/cor_custom/wizard/__init__.py @@ -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 diff --git a/cor_custom/wizard/crm_opportunity_to_quotation.py b/cor_custom/wizard/crm_opportunity_to_quotation.py new file mode 100755 index 0000000..22372da --- /dev/null +++ b/cor_custom/wizard/crm_opportunity_to_quotation.py @@ -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') + + + + diff --git a/cor_custom/wizard/project_create_sale_order.py b/cor_custom/wizard/project_create_sale_order.py new file mode 100755 index 0000000..fa39093 --- /dev/null +++ b/cor_custom/wizard/project_create_sale_order.py @@ -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.") + + + + + diff --git a/cor_custom/wizard/project_create_sale_order_views.xml b/cor_custom/wizard/project_create_sale_order_views.xml new file mode 100755 index 0000000..ae90675 --- /dev/null +++ b/cor_custom/wizard/project_create_sale_order_views.xml @@ -0,0 +1,16 @@ + + + + + project.create.sale.order.wizard.form.budgeted + project.create.sale.order + + + + + + + + + + diff --git a/mail_debrand/README.rst b/mail_debrand/README.rst new file mode 100755 index 0000000..9adf930 --- /dev/null +++ b/mail_debrand/README.rst @@ -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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa +* Eficent +* Onestein + +Contributors +~~~~~~~~~~~~ + +* Pedro M. Baeza +* Lois Rilo +* Graeme Gellatly + +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 `__: + +|maintainer-pedrobaeza| + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_debrand/__init__.py b/mail_debrand/__init__.py new file mode 100755 index 0000000..0650744 --- /dev/null +++ b/mail_debrand/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mail_debrand/__manifest__.py b/mail_debrand/__manifest__.py new file mode 100755 index 0000000..181256b --- /dev/null +++ b/mail_debrand/__manifest__.py @@ -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 +# 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"], +} diff --git a/mail_debrand/i18n/fr.po b/mail_debrand/i18n/fr.po new file mode 100755 index 0000000..7483801 --- /dev/null +++ b/mail_debrand/i18n/fr.po @@ -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 \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" diff --git a/mail_debrand/i18n/mail_debrand.pot b/mail_debrand/i18n/mail_debrand.pot new file mode 100755 index 0000000..7d25532 --- /dev/null +++ b/mail_debrand/i18n/mail_debrand.pot @@ -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 "" diff --git a/mail_debrand/i18n/nl.po b/mail_debrand/i18n/nl.po new file mode 100755 index 0000000..f300aac --- /dev/null +++ b/mail_debrand/i18n/nl.po @@ -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 \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 "" diff --git a/mail_debrand/i18n/pt.po b/mail_debrand/i18n/pt.po new file mode 100755 index 0000000..cfef66e --- /dev/null +++ b/mail_debrand/i18n/pt.po @@ -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 \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 "" diff --git a/mail_debrand/i18n/sl.po b/mail_debrand/i18n/sl.po new file mode 100755 index 0000000..0dc0471 --- /dev/null +++ b/mail_debrand/i18n/sl.po @@ -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 \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" diff --git a/mail_debrand/i18n/sr_Latn.po b/mail_debrand/i18n/sr_Latn.po new file mode 100755 index 0000000..4d97db0 --- /dev/null +++ b/mail_debrand/i18n/sr_Latn.po @@ -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 "" diff --git a/mail_debrand/models/__init__.py b/mail_debrand/models/__init__.py new file mode 100755 index 0000000..89e090b --- /dev/null +++ b/mail_debrand/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_template +from . import mail_thread diff --git a/mail_debrand/models/mail_template.py b/mail_debrand/models/mail_template.py new file mode 100755 index 0000000..a846eca --- /dev/null +++ b/mail_debrand/models/mail_template.py @@ -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"", "", 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) diff --git a/mail_debrand/models/mail_thread.py b/mail_debrand/models/mail_thread.py new file mode 100755 index 0000000..7fc6f0a --- /dev/null +++ b/mail_debrand/models/mail_thread.py @@ -0,0 +1,14 @@ +# Copyright 2019 Eficent Business and IT Consulting Services S.L. +# Lois Rilo +# 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 diff --git a/mail_debrand/readme/CONTRIBUTORS.rst b/mail_debrand/readme/CONTRIBUTORS.rst new file mode 100755 index 0000000..b5f7ce7 --- /dev/null +++ b/mail_debrand/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Pedro M. Baeza +* Lois Rilo +* Graeme Gellatly diff --git a/mail_debrand/readme/DESCRIPTION.rst b/mail_debrand/readme/DESCRIPTION.rst new file mode 100755 index 0000000..51b2b49 --- /dev/null +++ b/mail_debrand/readme/DESCRIPTION.rst @@ -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' diff --git a/mail_debrand/readme/HISTORY.rst b/mail_debrand/readme/HISTORY.rst new file mode 100755 index 0000000..ad209cc --- /dev/null +++ b/mail_debrand/readme/HISTORY.rst @@ -0,0 +1,4 @@ +12.0.1.0.0 (2018-11-06) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [NEW] Initial V12 version. Complete rewrite from v11. diff --git a/mail_debrand/readme/USAGE.rst b/mail_debrand/readme/USAGE.rst new file mode 100755 index 0000000..2a57dc0 --- /dev/null +++ b/mail_debrand/readme/USAGE.rst @@ -0,0 +1,5 @@ +To use this module, you need to: + +* Install it. +* Send an email. +* Nobody will know it comes from Odoo. diff --git a/mail_debrand/static/description/icon.png b/mail_debrand/static/description/icon.png new file mode 100755 index 0000000..06a30af Binary files /dev/null and b/mail_debrand/static/description/icon.png differ diff --git a/mail_debrand/static/description/icon.svg b/mail_debrand/static/description/icon.svg new file mode 100755 index 0000000..6609694 --- /dev/null +++ b/mail_debrand/static/description/icon.svg @@ -0,0 +1,248 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mail_debrand/static/description/index.html b/mail_debrand/static/description/index.html new file mode 100755 index 0000000..6db863b --- /dev/null +++ b/mail_debrand/static/description/index.html @@ -0,0 +1,449 @@ + + + + + + +Mail Debrand + + + +
+

Mail Debrand

+ + +

Production/Stable License: AGPL-3 OCA/social Translate me on Weblate Try me on Runbot

+

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

+ +
+

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. +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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
  • Eficent
  • +
  • Onestein
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

pedrobaeza

+

This module is part of the OCA/social project on GitHub.

+

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

+
+
+
+ + diff --git a/mail_debrand/tests/__init__.py b/mail_debrand/tests/__init__.py new file mode 100755 index 0000000..e7ef9cb --- /dev/null +++ b/mail_debrand/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_debrand diff --git a/mail_debrand/tests/test_mail_debrand.py b/mail_debrand/tests/test_mail_debrand.py new file mode 100755 index 0000000..3067c5e --- /dev/null +++ b/mail_debrand/tests/test_mail_debrand.py @@ -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) diff --git a/odoo-debranding/__init__.py b/odoo-debranding/__init__.py new file mode 100755 index 0000000..a03bfd0 --- /dev/null +++ b/odoo-debranding/__init__.py @@ -0,0 +1 @@ +from . import controllers \ No newline at end of file diff --git a/odoo-debranding/__manifest__.py b/odoo-debranding/__manifest__.py new file mode 100755 index 0000000..29c503e --- /dev/null +++ b/odoo-debranding/__manifest__.py @@ -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, + +} diff --git a/odoo-debranding/controllers/__init__.py b/odoo-debranding/controllers/__init__.py new file mode 100755 index 0000000..757b12a --- /dev/null +++ b/odoo-debranding/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/odoo-debranding/controllers/main.py b/odoo-debranding/controllers/main.py new file mode 100755 index 0000000..087c02a --- /dev/null +++ b/odoo-debranding/controllers/main.py @@ -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) diff --git a/odoo-debranding/models/__init__.py b/odoo-debranding/models/__init__.py new file mode 100755 index 0000000..f303b1b --- /dev/null +++ b/odoo-debranding/models/__init__.py @@ -0,0 +1 @@ +from . import odoodebrand \ No newline at end of file diff --git a/odoo-debranding/models/odoodebrand.py b/odoo-debranding/models/odoodebrand.py new file mode 100755 index 0000000..2677e05 --- /dev/null +++ b/odoo-debranding/models/odoodebrand.py @@ -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) + diff --git a/odoo-debranding/static/src/img/image.png b/odoo-debranding/static/src/img/image.png new file mode 100755 index 0000000..aef3194 Binary files /dev/null and b/odoo-debranding/static/src/img/image.png differ diff --git a/odoo-debranding/static/src/xml/base.xml b/odoo-debranding/static/src/xml/base.xml new file mode 100755 index 0000000..e39ef70 --- /dev/null +++ b/odoo-debranding/static/src/xml/base.xml @@ -0,0 +1,34 @@ + + + + + Your permission is required to enable desktop notifications. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/odoo-debranding/views/database_manager.html b/odoo-debranding/views/database_manager.html new file mode 100755 index 0000000..4cc77b5 --- /dev/null +++ b/odoo-debranding/views/database_manager.html @@ -0,0 +1,422 @@ + + + + + + Odoo + + + + + + + + + + + + + + + + + + + + + + + + + + {% macro master_input(set_master_pwd=False) -%} + + {% set input_class = "form-control" %} + {% if insecure %} + {% if set_master_pwd %} + + {% else %} +
+

Warning, your Odoo database manager is not protected. To secure it, we have generated the following master password for it:

+

+

You can change it below but be sure to remember it, it will be asked for future operations on databases.

+
+ {% set input_class = "form-control generated_master_pwd_input" %} + {% endif %} + {% endif %} + {% if not insecure or not set_master_pwd %} +
+ +
+ +
+ +
+
+
+ {% endif %} + {%- endmacro %} + + {% macro create_form() -%} + {{ master_input() }} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ {%- endmacro %} + + +
+ +
+
+ + {% if not list_db %} +
The database manager has been disabled by the administrator
+ {% elif insecure and databases %} +
+ Warning, your Odoo database manager is not protected.
+ Please set a master password to secure it. +
+ {% endif %} + {% if error %} +
{{ error }}
+ {% endif %} + {% if list_db and databases %} +
+ {% for db in databases %} +
+ + {% if db in incompatible_databases %} + + {% endif %} + {{ db }} + + {% if manage %} +
+ + + +
+ {% endif %} +
+ {% endfor %} +
+ {% if manage %} +
+ + + +
+ {% else %} + + {% endif %} + {% elif list_db %} +
+ {{ create_form() }} + +
+ or restore a database + {% endif %} +
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/odoo-debranding/views/webclient_templates.xml b/odoo-debranding/views/webclient_templates.xml new file mode 100755 index 0000000..5ab5f35 --- /dev/null +++ b/odoo-debranding/views/webclient_templates.xml @@ -0,0 +1,65 @@ + + +