From 44e47f76a16ea36b1ec644a4d75558dbfae93791 Mon Sep 17 00:00:00 2001 From: Haresh Chavda Date: Mon, 20 Aug 2018 14:59:49 +0530 Subject: [PATCH 1/5] [ADD]: Digest KPI module --- addons/account/__manifest__.py | 4 +- addons/account/data/digest_data.xml | 6 + addons/account/models/__init__.py | 1 + addons/account/models/digest.py | 29 +++ addons/account/views/digest_views.xml | 15 ++ addons/crm/__manifest__.py | 5 +- addons/crm/data/digest_data.xml | 29 +++ addons/crm/models/__init__.py | 1 + addons/crm/models/digest.py | 46 ++++ addons/crm/views/digest_views.xml | 16 ++ addons/digest/__init__.py | 6 + addons/digest/__manifest__.py | 27 +++ addons/digest/controllers/__init__.py | 4 + addons/digest/controllers/portal.py | 15 ++ addons/digest/data/digest_data.xml | 45 ++++ addons/digest/data/digest_template_data.xml | 154 +++++++++++++ addons/digest/data/ir_cron_data.xml | 13 ++ .../digest/data/res_config_settings_data.xml | 11 + addons/digest/models/__init__.py | 7 + addons/digest/models/digest.py | 209 ++++++++++++++++++ addons/digest/models/digest_tip.py | 21 ++ addons/digest/models/res_config_settings.py | 10 + addons/digest/models/res_users.py | 18 ++ addons/digest/security/ir.model.access.csv | 5 + addons/digest/static/src/img/activity.png | Bin 0 -> 25238 bytes addons/digest/static/src/img/app_store.png | Bin 0 -> 14016 bytes addons/digest/static/src/img/google_play.png | Bin 0 -> 16858 bytes addons/digest/static/src/img/notification.png | Bin 0 -> 29460 bytes addons/digest/views/digest_templates.xml | 16 ++ addons/digest/views/digest_views.xml | 132 +++++++++++ .../views/res_config_settings_views.xml | 32 +++ addons/digest/wizard/__init__.py | 4 + addons/digest/wizard/digest_custom_fields.py | 137 ++++++++++++ .../wizard/digest_custom_fields_view.xml | 34 +++ addons/hr_recruitment/__manifest__.py | 3 + addons/hr_recruitment/data/digest_data.xml | 25 +++ addons/hr_recruitment/models/__init__.py | 1 + addons/hr_recruitment/models/digest.py | 29 +++ addons/hr_recruitment/views/digest_views.xml | 15 ++ addons/point_of_sale/__manifest__.py | 4 +- addons/point_of_sale/data/digest_data.xml | 6 + addons/point_of_sale/models/__init__.py | 1 + addons/point_of_sale/models/digest.py | 29 +++ addons/point_of_sale/views/digest_views.xml | 15 ++ addons/project/__manifest__.py | 3 + addons/project/data/digest_data.xml | 26 +++ addons/project/models/__init__.py | 1 + addons/project/models/digest.py | 29 +++ addons/project/views/digest_views.xml | 15 ++ addons/resource/models/resource.py | 4 + addons/sale_expense/__manifest__.py | 1 + addons/sale_expense/data/digest_data.xml | 17 ++ addons/sale_management/__init__.py | 1 + addons/sale_management/__manifest__.py | 4 +- addons/sale_management/data/digest_data.xml | 6 + addons/sale_management/models/__init__.py | 4 + addons/sale_management/models/digest.py | 28 +++ addons/sale_management/views/digest_views.xml | 17 ++ addons/website_sale/__manifest__.py | 4 +- addons/website_sale/data/digest_data.xml | 6 + addons/website_sale/models/__init__.py | 1 + addons/website_sale/models/digest.py | 31 +++ addons/website_sale/views/digest_views.xml | 17 ++ 63 files changed, 1390 insertions(+), 5 deletions(-) create mode 100644 addons/account/data/digest_data.xml create mode 100644 addons/account/models/digest.py create mode 100644 addons/account/views/digest_views.xml create mode 100644 addons/crm/data/digest_data.xml create mode 100644 addons/crm/models/digest.py create mode 100644 addons/crm/views/digest_views.xml create mode 100644 addons/digest/__init__.py create mode 100644 addons/digest/__manifest__.py create mode 100644 addons/digest/controllers/__init__.py create mode 100644 addons/digest/controllers/portal.py create mode 100644 addons/digest/data/digest_data.xml create mode 100644 addons/digest/data/digest_template_data.xml create mode 100644 addons/digest/data/ir_cron_data.xml create mode 100644 addons/digest/data/res_config_settings_data.xml create mode 100644 addons/digest/models/__init__.py create mode 100644 addons/digest/models/digest.py create mode 100644 addons/digest/models/digest_tip.py create mode 100644 addons/digest/models/res_config_settings.py create mode 100644 addons/digest/models/res_users.py create mode 100644 addons/digest/security/ir.model.access.csv create mode 100644 addons/digest/static/src/img/activity.png create mode 100644 addons/digest/static/src/img/app_store.png create mode 100644 addons/digest/static/src/img/google_play.png create mode 100644 addons/digest/static/src/img/notification.png create mode 100644 addons/digest/views/digest_templates.xml create mode 100644 addons/digest/views/digest_views.xml create mode 100644 addons/digest/views/res_config_settings_views.xml create mode 100644 addons/digest/wizard/__init__.py create mode 100644 addons/digest/wizard/digest_custom_fields.py create mode 100644 addons/digest/wizard/digest_custom_fields_view.xml create mode 100644 addons/hr_recruitment/data/digest_data.xml create mode 100644 addons/hr_recruitment/models/digest.py create mode 100644 addons/hr_recruitment/views/digest_views.xml create mode 100644 addons/point_of_sale/data/digest_data.xml create mode 100644 addons/point_of_sale/models/digest.py create mode 100644 addons/point_of_sale/views/digest_views.xml create mode 100644 addons/project/data/digest_data.xml create mode 100644 addons/project/models/digest.py create mode 100644 addons/project/views/digest_views.xml create mode 100644 addons/sale_expense/data/digest_data.xml create mode 100644 addons/sale_management/data/digest_data.xml create mode 100644 addons/sale_management/models/__init__.py create mode 100644 addons/sale_management/models/digest.py create mode 100644 addons/sale_management/views/digest_views.xml create mode 100644 addons/website_sale/data/digest_data.xml create mode 100644 addons/website_sale/models/digest.py create mode 100644 addons/website_sale/views/digest_views.xml diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 30eb5120..541dbb35 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -12,12 +12,13 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'category': 'Accounting', 'website': 'https://flectrahq.com/accounting', 'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'], - 'depends' : ['base_setup', 'product', 'analytic', 'web_planner', 'portal'], + 'depends' : ['base_setup', 'product', 'analytic', 'web_planner', 'portal', 'digest'], 'data': [ 'security/account_security.xml', 'security/ir.model.access.csv', 'data/data_account_type.xml', 'data/account_data.xml', + 'data/digest_data.xml', 'views/account_menuitem.xml', 'views/account_payment_view.xml', 'wizard/account_reconcile_view.xml', @@ -67,6 +68,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'views/account_dashboard_setup_bar.xml', 'wizard/account_report_tax_view.xml', 'views/report_tax.xml', + 'views/digest_views.xml', ], 'demo': [ 'demo/account_demo.xml', diff --git a/addons/account/data/digest_data.xml b/addons/account/data/digest_data.xml new file mode 100644 index 00000000..c02db76a --- /dev/null +++ b/addons/account/data/digest_data.xml @@ -0,0 +1,6 @@ + + + + True + + diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 399daaa3..37fc53e1 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -14,3 +14,4 @@ from . import company from . import res_config_settings from . import web_planner from . import account_cash_rounding +from . import digest diff --git a/addons/account/models/digest.py b/addons/account/models/digest.py new file mode 100644 index 00000000..f50be17a --- /dev/null +++ b/addons/account/models/digest.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_account_total_revenue = fields.Boolean('Revenue') + kpi_account_total_revenue_value = fields.Monetary(compute='_compute_kpi_account_total_revenue_value') + + def _compute_kpi_account_total_revenue_value(self): + if not self.env.user.has_group('account.group_account_invoice'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + account_moves = self.env['account.move'].read_group([ + ('journal_id.type', '=', 'sale'), + ('company_id', '=', company.id), + ('date', '>=', start), + ('date', '<', end)], ['journal_id', 'amount'], ['journal_id']) + record.kpi_account_total_revenue_value = sum([account_move['amount'] for account_move in account_moves]) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_account_total_revenue'] = 'account.action_invoice_tree1&menu_id=%s' % self.env.ref('account.menu_finance').id + return res diff --git a/addons/account/views/digest_views.xml b/addons/account/views/digest_views.xml new file mode 100644 index 00000000..9f86087b --- /dev/null +++ b/addons/account/views/digest_views.xml @@ -0,0 +1,15 @@ + + + + digest.digest.view.form.inherit.account.account + digest.digest + + + + + + + + + + diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 0d48dba1..d0958782 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -20,7 +20,8 @@ 'utm', 'web_planner', 'web_tour', - 'contacts' + 'contacts', + 'digest', ], 'data': [ 'security/crm_security.xml', @@ -29,6 +30,7 @@ 'data/crm_data.xml', 'data/crm_stage_data.xml', 'data/crm_lead_data.xml', + 'data/digest_data.xml', 'data/mail_template_data.xml', 'wizard/base_partner_merge_views.xml', @@ -48,6 +50,7 @@ 'report/crm_activity_report_views.xml', 'report/crm_opportunity_report_views.xml', 'views/crm_team_views.xml', + 'views/digest_views.xml', ], 'demo': [ 'data/crm_demo.xml', diff --git a/addons/crm/data/digest_data.xml b/addons/crm/data/digest_data.xml new file mode 100644 index 00000000..076c795a --- /dev/null +++ b/addons/crm/data/digest_data.xml @@ -0,0 +1,29 @@ + + + + + True + True + + + + + + 2 + + +
+ % set email = object.env['crm.team'].search([('alias_name','!=', False)],limit=1).alias_id.display_name + % if email + Try the mail gateway +
Email sent to ${email} generate opportunities in your pipeline.
+
+ Try Now +
+
+ % endif +
+
+
+
+
diff --git a/addons/crm/models/__init__.py b/addons/crm/models/__init__.py index dd775c6e..7a073ebb 100644 --- a/addons/crm/models/__init__.py +++ b/addons/crm/models/__init__.py @@ -9,3 +9,4 @@ from . import crm_team from . import res_config_settings from . import res_partner from . import web_planner +from . import digest diff --git a/addons/crm/models/digest.py b/addons/crm/models/digest.py new file mode 100644 index 00000000..186be983 --- /dev/null +++ b/addons/crm/models/digest.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_crm_lead_created = fields.Boolean('New Leads/Opportunities') + kpi_crm_lead_created_value = fields.Integer(compute='_compute_kpi_crm_lead_created_value') + kpi_crm_opportunities_won = fields.Boolean('Opportunities Won') + kpi_crm_opportunities_won_value = fields.Integer(compute='_compute_kpi_crm_opportunities_won_value') + + def _compute_kpi_crm_lead_created_value(self): + if not self.env.user.has_group('sales_team.group_sale_salesman'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + record.kpi_crm_lead_created_value = self.env['crm.lead'].search_count([ + ('create_date', '>=', start), + ('create_date', '<', end), + ('company_id', '=', company.id) + ]) + + def _compute_kpi_crm_opportunities_won_value(self): + if not self.env.user.has_group('sales_team.group_sale_salesman'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + record.kpi_crm_opportunities_won_value = self.env['crm.lead'].search_count([ + ('type', '=', 'opportunity'), + ('probability', '=', '100'), + ('date_closed', '>=', start), + ('date_closed', '<', end), + ('company_id', '=', company.id) + ]) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_crm_lead_created'] = 'crm.crm_lead_opportunities_tree_view&menu_id=%s' % self.env.ref('crm.crm_menu_root').id + res['kpi_crm_opportunities_won'] = 'crm.crm_lead_opportunities_tree_view&menu_id=%s' % self.env.ref('crm.crm_menu_root').id + if user.has_group('crm.group_use_lead'): + res['kpi_crm_lead_created'] = 'crm.crm_lead_all_leads&menu_id=%s' % self.env.ref('crm.crm_menu_root').id + return res diff --git a/addons/crm/views/digest_views.xml b/addons/crm/views/digest_views.xml new file mode 100644 index 00000000..7dcdbaaa --- /dev/null +++ b/addons/crm/views/digest_views.xml @@ -0,0 +1,16 @@ + + + + digest.digest.view.form.inherit.crm.lead + digest.digest + + + + + + + + + + + diff --git a/addons/digest/__init__.py b/addons/digest/__init__.py new file mode 100644 index 00000000..77a58516 --- /dev/null +++ b/addons/digest/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models +from . import wizard \ No newline at end of file diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py new file mode 100644 index 00000000..1541460e --- /dev/null +++ b/addons/digest/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +{ + 'name': 'KPI Digests', + 'category': 'Marketing', + 'description': """ +Send KPI Digests periodically +============================= +""", + 'version': '1.0', + 'depends': [ + 'mail', + 'portal' + ], + 'data': [ + 'security/ir.model.access.csv', + 'data/digest_template_data.xml', + 'data/digest_data.xml', + 'data/ir_cron_data.xml', + 'data/res_config_settings_data.xml', + 'views/digest_views.xml', + 'views/digest_templates.xml', + 'views/res_config_settings_views.xml', + 'wizard/digest_custom_fields_view.xml', + ], + 'installable': True, +} diff --git a/addons/digest/controllers/__init__.py b/addons/digest/controllers/__init__.py new file mode 100644 index 00000000..e344144f --- /dev/null +++ b/addons/digest/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from . import portal diff --git a/addons/digest/controllers/portal.py b/addons/digest/controllers/portal.py new file mode 100644 index 00000000..147b2ad5 --- /dev/null +++ b/addons/digest/controllers/portal.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra.http import Controller, request, route + + +class DigestController(Controller): + + @route('/digest//unsubscribe', type='http', website=True, auth='user') + def digest_unsubscribe(self, digest_id, **post): + digest = request.env['digest.digest'].sudo().browse(digest_id) + digest.action_unsubcribe() + return request.render('digest.portal_digest_unsubscribed', { + 'digest': digest, + }) diff --git a/addons/digest/data/digest_data.xml b/addons/digest/data/digest_data.xml new file mode 100644 index 00000000..6e806997 --- /dev/null +++ b/addons/digest/data/digest_data.xml @@ -0,0 +1,45 @@ + + + + + Weekly Digest + + + True + True + + + + + + 1 + +
+ % set users = object.env['res.users'].search([], limit=10, order='id desc') + % set channel_id = object.env.ref('mail.channel_all_employees').id + Did you know...? +
You can ping colleagues by tagging them in your messages using "@". They will be instantly notified.
+
+
+
+
+ ${', '.join(users.mapped('name'))} signed up. Say hello in the company's discussion channel. +
+
+
+
+
+ + 7 + +
+ Get things done with activities +
You don't have any activity scheduled. Use activities on any business document to schedule meetings, calls and todos.
+
+
+
+
+
+
+
+
diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml new file mode 100644 index 00000000..0f41eae5 --- /dev/null +++ b/addons/digest/data/digest_template_data.xml @@ -0,0 +1,154 @@ + + + + Digest: Default main template + + + ${user.email} + ${user.lang} + + + + % set company, user = ctx['company'], ctx['user'] + % set data = object.compute_kpis(company, user) + % set tips = object.compute_tips(company, user) + % set kpi_actions = object.compute_kpis_actions(company, user) + % set kpis = data.yesterday.keys() + + + + + + +
+ ${company.name} at a glance +
${datetime.date.today().strftime('%B %d, %Y')}
+
+ +
+
+
+ % for kpi in kpis: + + + + + + + +

+ ${object.fields_get()[kpi]['string']} + %if kpi in kpi_actions: + + View more + + %endif +
+ + + + + + +
+ + + + +
+ ${data['yesterday'][kpi][kpi]}
+ Yesterday + % if data['yesterday'][kpi]['margin'] != 0.0: + + % if data['yesterday'][kpi]['margin'] > 0.0: + ${"%.2f" % data['yesterday'][kpi]['margin']} % + % endif + % if data['yesterday'][kpi]['margin'] < 0.0: + ${"%.2f" % data['yesterday'][kpi]['margin']} % + % endif + + % endif +
+
+ + + + +
+ ${data['lastweek'][kpi][kpi]}
+ Last 7 Days + % if data['lastweek'][kpi]['margin'] != 0.0: + + % if data['lastweek'][kpi]['margin'] > 0.0: + ${"%.2f" % data['lastweek'][kpi]['margin']} % + % endif + % if data['lastweek'][kpi]['margin'] < 0.0: + ${"%.2f" % data['lastweek'][kpi]['margin']} % + %endif + + %endif +
+
+ + + + +
+ ${data['lastmonth'][kpi][kpi]}
+ Last 30 Days + % if data['lastmonth'][kpi]['margin'] != 0.0: + + % if data['lastmonth'][kpi]['margin'] > 0.0: + ${"%.2f" % data['lastmonth'][kpi]['margin']} % + % endif + % if data['lastmonth'][kpi]['margin'] < 0.0: + ${"%.2f" % data['lastmonth'][kpi]['margin']} % + %endif + + %endif +
+
+
+ % endfor + % if tips: + + + + +

+
${ctx['tip_description']|safe}
+
+ % endif + + + + + + + +

+
Run your bussiness from anywhere with Flectra Mobile.
+
+
+
+
+ + + + +
+ % if ctx['user'].has_group('base.group_system'): +
+ Want to customize the email? + Choose the metrics you care about +
+
+ % endif +
+ + + + ]]>
+
+
\ No newline at end of file diff --git a/addons/digest/data/ir_cron_data.xml b/addons/digest/data/ir_cron_data.xml new file mode 100644 index 00000000..39e637a7 --- /dev/null +++ b/addons/digest/data/ir_cron_data.xml @@ -0,0 +1,13 @@ + + + + Digest Emails + + code + model._cron_send_digest_email() + + 1 + days + -1 + + diff --git a/addons/digest/data/res_config_settings_data.xml b/addons/digest/data/res_config_settings_data.xml new file mode 100644 index 00000000..3aeaa563 --- /dev/null +++ b/addons/digest/data/res_config_settings_data.xml @@ -0,0 +1,11 @@ + + + + digest.default_digest_emails + True + + + digest.default_digest_id + + + diff --git a/addons/digest/models/__init__.py b/addons/digest/models/__init__.py new file mode 100644 index 00000000..0c968df8 --- /dev/null +++ b/addons/digest/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from . import digest +from . import digest_tip +from . import res_config_settings +from . import res_users diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py new file mode 100644 index 00000000..d46c5eca --- /dev/null +++ b/addons/digest/models/digest.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +import logging +import math +import pytz + +from datetime import datetime, date +from dateutil.relativedelta import relativedelta + +from flectra import api, fields, models, tools +from flectra.addons.base.ir.ir_mail_server import MailDeliveryException +from flectra.exceptions import AccessError +from flectra.tools.float_utils import float_round + +_logger = logging.getLogger(__name__) + + +class Digest(models.Model): + _name = 'digest.digest' + _description = 'Digest' + + # Digest description + name = fields.Char(string='Name', required=True, translate=True) + user_ids = fields.Many2many('res.users', string='Recipients', domain="[('share', '=', False)]") + periodicity = fields.Selection([('daily', 'Daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('quarterly', 'Quarterly')], + string='Periodicity', default='weekly', required=True) + next_run_date = fields.Date(string='Next Send Date') + template_id = fields.Many2one('mail.template', string='Email Template', + domain="[('model','=','digest.digest')]", + default=lambda self: self.env.ref('digest.digest_mail_template'), + required=True) + currency_id = fields.Many2one(related="company_id.currency_id", string='Currency') + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id.id) + available_fields = fields.Char(compute='_compute_available_fields') + is_subscribed = fields.Boolean('Is user subscribed', compute='_compute_is_subscribed') + state = fields.Selection([('activated', 'Activated'), ('deactivated', 'Deactivated')], string='Status', readonly=True, default='activated') + # First base-related KPIs + kpi_res_users_connected = fields.Boolean('Connected Users') + kpi_res_users_connected_value = fields.Integer(compute='_compute_kpi_res_users_connected_value') + kpi_mail_message_total = fields.Boolean('Messages') + kpi_mail_message_total_value = fields.Integer(compute='_compute_kpi_mail_message_total_value') + + + def _compute_is_subscribed(self): + for digest in self: + digest.is_subscribed = self.env.user in digest.user_ids + + def _compute_available_fields(self): + for digest in self: + kpis_values_fields = [] + for field_name, field in digest._fields.items(): + if field.type == 'boolean' and (field_name.startswith('kpi_') or field_name.startswith('x_kpi_')) and digest[field_name]: + kpis_values_fields += [field_name + '_value'] + digest.available_fields = ', '.join(kpis_values_fields) + + def _get_kpi_compute_parameters(self): + return fields.Date.to_string(self._context.get('start_date')), fields.Date.to_string(self._context.get('end_date')), self._context.get('company') + + def _compute_kpi_res_users_connected_value(self): + for record in self: + start, end, company = record._get_kpi_compute_parameters() + user_connected = self.env['res.users'].search_count([('company_id', '=', company.id), ('login_date', '>=', start), ('login_date', '<', end)]) + record.kpi_res_users_connected_value = user_connected + + def _compute_kpi_mail_message_total_value(self): + for record in self: + start, end, company = record._get_kpi_compute_parameters() + total_messages = self.env['mail.message'].search_count([('create_date', '>=',start), ('create_date', '<', end)]) + record.kpi_mail_message_total_value = total_messages + + @api.onchange('periodicity') + def _onchange_periodicity(self): + self.next_run_date = self._get_next_run_date() + + @api.model + def create(self, vals): + vals['next_run_date'] = date.today() + relativedelta(days=3) + return super(Digest, self).create(vals) + + @api.multi + def action_subscribe(self): + if self.env.user not in self.user_ids: + self.sudo().user_ids |= self.env.user + + @api.multi + def action_unsubcribe(self): + if self.env.user in self.user_ids: + self.sudo().user_ids -= self.env.user + + @api.multi + def action_activate(self): + self.state = 'activated' + + @api.multi + def action_deactivate(self): + self.state = 'deactivated' + + def action_send(self): + for digest in self: + for user in digest.user_ids: + subject = '%s: %s' % (user.company_id.name, digest.name) + digest.template_id.with_context(user=user, company=user.company_id).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) + digest.next_run_date = digest._get_next_run_date() + + def compute_kpis(self, company, user): + self.ensure_one() + res = {} + for tf_name, tf in self._compute_timeframes(company).items(): + digest = self.with_context(start_date=tf[0][0], end_date=tf[0][1], company=company).sudo(user.id) + previous_digest = self.with_context(start_date=tf[1][0], end_date=tf[1][1], company=company).sudo(user.id) + kpis = {} + for field_name, field in self._fields.items(): + if field.type == 'boolean' and (field_name.startswith('kpi_') or field_name.startswith('x_kpi_')) and self[field_name]: + + try: + compute_value = digest[field_name + '_value'] + previous_value = previous_digest[field_name + '_value'] + except AccessError: # no access rights -> just skip that digest details from that user's digest email + continue + margin = self._get_margin_value(compute_value, previous_value) + if self._fields[field_name+'_value'].type == 'monetary': + converted_amount = self._format_human_readable_amount(compute_value) + kpis.update({field_name: {field_name: self._format_currency_amount(converted_amount, company.currency_id), 'margin': margin}}) + else: + kpis.update({field_name: {field_name: compute_value, 'margin': margin}}) + + res.update({tf_name: kpis}) + return res + + def compute_tips(self, company, user): + tip = self.env['digest.tip'].search([('user_ids', '!=', user.id), '|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)], limit=1) + if not tip: + return False + tip.user_ids = [4, user.id] + body = tools.html_sanitize(tip.tip_description) + tip_description = self.env['mail.template'].render_template(body, 'digest.tip', self.id) + return tip_description + + def compute_kpis_actions(self, company, user): + """ Give an optional action to display in digest email linked to some KPIs. + + :return dict: key: kpi name (field name), value: an action that will be + concatenated with /web#action={action} + """ + return {} + + def _get_next_run_date(self): + self.ensure_one() + if self.periodicity == 'daily': + delta = relativedelta(days=1) + elif self.periodicity == 'weekly': + delta = relativedelta(weeks=1) + elif self.periodicity == 'monthly': + delta = relativedelta(months=1) + elif self.periodicity == 'quarterly': + delta = relativedelta(months=3) + return date.today() + delta + + def _compute_timeframes(self, company): + now = datetime.utcnow() + tz_name = company.resource_calendar_id.tz + if tz_name: + now = pytz.timezone(tz_name).localize(now) + start_date = now.date() + return { + 'yesterday': ( + (start_date + relativedelta(days=-1), start_date), + (start_date + relativedelta(days=-2), start_date + relativedelta(days=-1))), + 'lastweek': ( + (start_date + relativedelta(weeks=-1), start_date), + (start_date + relativedelta(weeks=-2), start_date + relativedelta(weeks=-1))), + 'lastmonth': ( + (start_date + relativedelta(months=-1), start_date), + (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))), + } + + def _get_margin_value(self, value, previous_value=0.0): + margin = 0.0 + if (value != previous_value) and (value != 0.0 and previous_value != 0.0): + margin = float_round((float(value-previous_value) / previous_value or 1) * 100, precision_digits=2) + return margin + + def _format_currency_amount(self, amount, currency_id): + pre = post = u'' + if currency_id.position == 'before': + pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency_id.symbol or '') + else: + post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency_id.symbol or '') + return u'{pre}{0}{post}'.format(amount, pre=pre, post=post) + + def _format_human_readable_amount(self, amount, suffix=''): + for unit in ['', 'K', 'M', 'G']: + if abs(amount) < 1000.0: + return "%3.1f%s%s" % (amount, unit, suffix) + amount /= 1000.0 + return "%.1f%s%s" % (amount, 'T', suffix) + + @api.model + def _cron_send_digest_email(self): + digests = self.search([('next_run_date', '=', fields.Date.today()), ('state', '=', 'activated')]) + for digest in digests: + try: + digest.action_send() + except MailDeliveryException as e: + _logger.warning('MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.') diff --git a/addons/digest/models/digest_tip.py b/addons/digest/models/digest_tip.py new file mode 100644 index 00000000..6679ea2c --- /dev/null +++ b/addons/digest/models/digest_tip.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from flectra import fields, models +from flectra.tools.translate import html_translate + + +class DigestTip(models.Model): + _name = 'digest.tip' + _description = 'Digest Tips' + _order = 'sequence' + + sequence = fields.Integer( + 'Sequence', default=1, + help='Used to display digest tip in email template base on order') + user_ids = fields.Many2many( + 'res.users', string='Recipients', + help='Users having already received this tip') + tip_description = fields.Html('Tip description', translate=html_translate) + group_id = fields.Many2one( + 'res.groups', string='Authorized Group', + default=lambda self: self.env.ref('base.group_user')) diff --git a/addons/digest/models/res_config_settings.py b/addons/digest/models/res_config_settings.py new file mode 100644 index 00000000..5ff0b31b --- /dev/null +++ b/addons/digest/models/res_config_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from flectra import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + digest_emails = fields.Boolean(string="Digest Emails", config_parameter='digest.default_digest_emails') + digest_id = fields.Many2one('digest.digest', string='Digest Email', config_parameter='digest.default_digest_id') diff --git a/addons/digest/models/res_users.py b/addons/digest/models/res_users.py new file mode 100644 index 00000000..34e12593 --- /dev/null +++ b/addons/digest/models/res_users.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from flectra import api, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + @api.model + def create(self, vals): + """ Automatically subscribe employee users to default digest if activated """ + user = super(ResUsers, self).create(vals) + default_digest_emails = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_emails') + default_digest_id = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_id') + if user.has_group('base.group_user') and default_digest_emails and default_digest_id: + digest = self.env['digest.digest'].sudo().browse(int(default_digest_id)) + digest.user_ids |= user + return user diff --git a/addons/digest/security/ir.model.access.csv b/addons/digest/security/ir.model.access.csv new file mode 100644 index 00000000..2891b43b --- /dev/null +++ b/addons/digest/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_digest_digest_system,digest.digest.administration,model_digest_digest,base.group_erp_manager,1,1,1,1 +access_digest_digest_user,digest.digest.user,model_digest_digest,base.group_user,1,0,0,0 +access_digest_tip_system,digest.tip.administration,model_digest_tip,base.group_erp_manager,1,1,1,1 +access_digest_tip_user,digest.tip.user,model_digest_tip,base.group_user,0,0,0,0 diff --git a/addons/digest/static/src/img/activity.png b/addons/digest/static/src/img/activity.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b85043de39dce5a12a530a7ac56649685621dc GIT binary patch literal 25238 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 literal 0 HcmV?d00001 diff --git a/addons/digest/static/src/img/app_store.png b/addons/digest/static/src/img/app_store.png new file mode 100644 index 0000000000000000000000000000000000000000..65320714c06525b12d4e92bb43ed04318edc6c83 GIT binary patch literal 14016 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 BY}^0< literal 0 HcmV?d00001 diff --git a/addons/digest/static/src/img/google_play.png b/addons/digest/static/src/img/google_play.png new file mode 100644 index 0000000000000000000000000000000000000000..229d939d728a85944129cfe373e6a51e7d25768d GIT binary patch literal 16858 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 + + \ No newline at end of file diff --git a/addons/digest/views/digest_views.xml b/addons/digest/views/digest_views.xml new file mode 100644 index 00000000..cf61541f --- /dev/null +++ b/addons/digest/views/digest_views.xml @@ -0,0 +1,132 @@ + + + + digest.digest.view.tree + digest.digest + + + + + + + + + + digest.digest.view.form + digest.digest + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+ + digest.digest.view.search + digest.digest + + + + + + + + + + + + Digest Emails + digest.digest + form + + + + +
diff --git a/addons/digest/views/res_config_settings_views.xml b/addons/digest/views/res_config_settings_views.xml new file mode 100644 index 00000000..44656357 --- /dev/null +++ b/addons/digest/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + res.config.settings.view.form.inherit.digest + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/addons/digest/wizard/__init__.py b/addons/digest/wizard/__init__.py new file mode 100644 index 00000000..a10b73b9 --- /dev/null +++ b/addons/digest/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from . import digest_custom_fields \ No newline at end of file diff --git a/addons/digest/wizard/digest_custom_fields.py b/addons/digest/wizard/digest_custom_fields.py new file mode 100644 index 00000000..b190636b --- /dev/null +++ b/addons/digest/wizard/digest_custom_fields.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, fields, models, _ +from flectra.exceptions import ValidationError, UserError +from lxml import etree +from flectra.tools.safe_eval import test_python_expr + + +class DigestCustomFields(models.TransientModel): + _name = 'digest.custom.fields' + + + DEFAULT_PYTHON_CODE = """# Available variables: +# - env: Flectra Environment on which the action is triggered +# - model: Flectra Model of the record on which the action is triggered; is a void recordset +# - record: record on which the action is triggered; may be be void +# - records: recordset of all records on which the action is triggered in multi-mode; may be void +# - time, datetime, dateutil, timezone: useful Python libraries +# - log: log(message, level='info'): logging function to record debug information in ir.logging table +# - Warning: Warning Exception to use with raise +# To return an action, assign: action = {...}\n\n\n\n""" + + + # field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '=', False)]") + # compute_field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '!=', False)]") + + field_name = fields.Char('Field Name', default='x_kpi_', required=True) + label_name = fields.Char('Label Name', required=True) + group_name = fields.Char('Group Name', required=True) + ttype = fields.Selection([('integer', 'Integer'), ('monetary', 'Monetary')], string='Field Type', required=True) + # compute = fields.Text(help="Code to compute the value of the field.\n" + # "Iterate on the recordset 'self' and assign the field's value:\n\n" + # " for record in self:\n" + # " record['size'] = len(record.name)\n\n" + # "Modules time, datetime, dateutil are available.") + + + compute = fields.Text(string='Python Code', groups='base.group_system', + default=DEFAULT_PYTHON_CODE, + help="Write Python code that the action will execute. Some variables are " + "available for use; help about pyhon expression is given in the help tab.") + + compute_field_name = fields.Char(compute='_compute_get_field_name', string='Compute Field Name') + + @api.constrains('compute') + def _check_python_code(self): + for record in self.sudo().filtered('compute'): + msg = test_python_expr(expr=record.compute.strip(), mode="exec") + if msg: + raise ValidationError(msg) + + @api.depends('field_name') + def _compute_get_field_name(self): + for record in self: + if record.field_name: + record.compute_field_name = record.field_name + '_value' + + @api.constrains('field_name') + def _check_name(self): + for field in self: + if not field.field_name.startswith('x_kpi_'): + raise ValidationError(_("Custom fields must have a name that starts with 'x_kpi_' !")) + try: + models.check_pg_name(field.field_name) + except ValidationError: + msg = _("Field names can only contain characters, digits and underscores (up to 63).") + raise ValidationError(msg) + + def add_new_fields(self): + model_id = self.env['ir.model'].search([('model', '=', 'digest.digest')]) + ir_model_fields_obj = self.env['ir.model.fields'] + + first_field_name = self.field_name + values = { + 'model_id': model_id.id, + 'ttype': 'boolean', + 'name': first_field_name, + 'field_description': self.label_name, + 'model': 'digest.digest' + } + ir_model_fields_obj.create(values) + + values = { + 'model_id': model_id.id, + 'ttype': self.ttype, + 'name': self.field_name + '_value', + 'field_description': self.label_name + ' Value', + 'model': 'digest.digest', + 'depends': first_field_name, + 'compute': self.compute + } + print("====values=======", values) + ir_model_fields_obj.create(values) + + def field_arch(self): + xpath = etree.Element('xpath') + xpath_type = "group" + name = "kpi_general" + position = "after" + xpath_field = self.field_name + expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]' + xpath.set('expr', expr) + xpath.set('position', position) + if position == 'after' or position == 'before' or position == 'inside': + expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]' + group = etree.Element('group') + group.set('string', self.group_name) + field = etree.Element('field') + field.set('name', xpath_field) + xpath.set('expr', expr) + group.append(field) + xpath.append(group) + return etree.tostring(xpath).decode("utf-8") + + @api.multi + def action_customize_digest(self): + self.add_new_fields() + arch = '' + str(self.field_arch()) + '' + print("====arch=======", arch) + vals = { + 'type': 'form', + 'model': 'digest.digest', + 'inherit_id': self.env.ref('digest.digest_digest_view_form').id, + 'mode': 'extension', + 'arch_base': arch, + 'name': 'x_kpi_' + self.field_name + "_Customization", + } + ir_model = self.env['ir.model'].search([('model', '=', 'digest.digest')]) + if hasattr(ir_model, 'module_id'): + vals.update({'module_id': ir_model.module_id.id}) + self.env['ir.ui.view'].sudo().create(vals) + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } + diff --git a/addons/digest/wizard/digest_custom_fields_view.xml b/addons/digest/wizard/digest_custom_fields_view.xml new file mode 100644 index 00000000..732608c0 --- /dev/null +++ b/addons/digest/wizard/digest_custom_fields_view.xml @@ -0,0 +1,34 @@ + + + + digest.custom.fields.form + digest.custom.fields + +
+ + + + + + + + + + +
+
+
+
+
+ + + Customized Digest + ir.actions.act_window + digest.custom.fields + form + + new + +
diff --git a/addons/hr_recruitment/__manifest__.py b/addons/hr_recruitment/__manifest__.py index 57baf118..8fbeded2 100644 --- a/addons/hr_recruitment/__manifest__.py +++ b/addons/hr_recruitment/__manifest__.py @@ -17,16 +17,19 @@ 'utm', 'document', 'web_tour', + 'digest', ], 'data': [ 'security/hr_recruitment_security.xml', 'security/ir.model.access.csv', 'data/hr_recruitment_data.xml', + 'data/digest_data.xml', 'views/hr_recruitment_views.xml', 'views/res_config_settings_views.xml', 'views/hr_recruitment_templates.xml', 'views/hr_department_views.xml', 'views/hr_job_views.xml', + 'views/digest_views.xml', ], 'demo': [ 'data/hr_recruitment_demo.xml', diff --git a/addons/hr_recruitment/data/digest_data.xml b/addons/hr_recruitment/data/digest_data.xml new file mode 100644 index 00000000..b9050214 --- /dev/null +++ b/addons/hr_recruitment/data/digest_data.xml @@ -0,0 +1,25 @@ + + + + + True + + + + + + 4 + + +
+ + + + diff --git a/addons/hr_recruitment/models/__init__.py b/addons/hr_recruitment/models/__init__.py index 078e032e..6eca3380 100644 --- a/addons/hr_recruitment/models/__init__.py +++ b/addons/hr_recruitment/models/__init__.py @@ -4,3 +4,4 @@ from . import hr_employee from . import hr_job from . import res_config_settings from . import calendar +from . import digest diff --git a/addons/hr_recruitment/models/digest.py b/addons/hr_recruitment/models/digest.py new file mode 100644 index 00000000..380f3baa --- /dev/null +++ b/addons/hr_recruitment/models/digest.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_hr_recruitment_new_colleagues = fields.Boolean('Employees') + kpi_hr_recruitment_new_colleagues_value = fields.Integer(compute='_compute_kpi_hr_recruitment_new_colleagues_value') + + def _compute_kpi_hr_recruitment_new_colleagues_value(self): + if not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + new_colleagues = self.env['hr.employee'].search_count([ + ('create_date', '>=', start), + ('create_date', '<', end), + ('company_id', '=', company.id) + ]) + record.kpi_hr_recruitment_new_colleagues_value = new_colleagues + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_hr_recruitment_new_colleagues'] = 'hr.open_view_employee_list_my&menu_id=%s' % self.env.ref('hr.menu_hr_root').id + return res diff --git a/addons/hr_recruitment/views/digest_views.xml b/addons/hr_recruitment/views/digest_views.xml new file mode 100644 index 00000000..38ce8b4c --- /dev/null +++ b/addons/hr_recruitment/views/digest_views.xml @@ -0,0 +1,15 @@ + + + + digest.digest.view.form.inherit.hr.recruitment + digest.digest + + + + + + + + + + diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index aa81d7b0..9ef549ef 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -9,11 +9,12 @@ 'sequence': 20, 'summary': 'Touchscreen Interface for Shops', 'description': "", - 'depends': ['stock_account', 'barcodes', 'web_editor'], + 'depends': ['stock_account', 'barcodes', 'web_editor', 'digest'], 'data': [ 'security/point_of_sale_security.xml', 'security/ir.model.access.csv', 'data/default_barcode_patterns.xml', + 'data/digest_data.xml', 'wizard/pos_box.xml', 'wizard/pos_details.xml', 'wizard/pos_discount.xml', @@ -36,6 +37,7 @@ 'views/account_statement_view.xml', 'views/account_statement_report.xml', 'views/res_users_view.xml', + 'views/digest_views.xml', 'views/res_partner_view.xml', 'views/report_statement.xml', 'views/report_userlabel.xml', diff --git a/addons/point_of_sale/data/digest_data.xml b/addons/point_of_sale/data/digest_data.xml new file mode 100644 index 00000000..528f11db --- /dev/null +++ b/addons/point_of_sale/data/digest_data.xml @@ -0,0 +1,6 @@ + + + + True + + diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index 349a1bcc..3fcc7c5c 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -4,6 +4,7 @@ from . import account_bank_statement from . import account_journal from . import barcode_rule +from . import digest from . import pos_category from . import pos_config from . import pos_order diff --git a/addons/point_of_sale/models/digest.py b/addons/point_of_sale/models/digest.py new file mode 100644 index 00000000..b9919c15 --- /dev/null +++ b/addons/point_of_sale/models/digest.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_pos_total = fields.Boolean('POS Sales') + kpi_pos_total_value = fields.Monetary(compute='_compute_kpi_pos_total_value') + + def _compute_kpi_pos_total_value(self): + if not self.env.user.has_group('point_of_sale.group_pos_user'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + record.kpi_pos_total_value = sum(self.env['pos.order'].search([ + ('date_order', '>=', start), + ('date_order', '<', end), + ('state', 'not in', ['draft', 'cancel', 'invoiced']), + ('company_id', '=', company.id) + ]).mapped('amount_total')) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_pos_total'] = 'point_of_sale.action_pos_sale_graph&menu_id=%s' % self.env.ref('point_of_sale.menu_point_root').id + return res diff --git a/addons/point_of_sale/views/digest_views.xml b/addons/point_of_sale/views/digest_views.xml new file mode 100644 index 00000000..c361d6d2 --- /dev/null +++ b/addons/point_of_sale/views/digest_views.xml @@ -0,0 +1,15 @@ + + + + digest.digest.view.form.inherit.sale.order + digest.digest + + + + + + + + + + diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index ecbce438..744b496e 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -19,6 +19,7 @@ 'web', 'web_planner', 'web_tour', + 'digest' ], 'description': "", 'data': [ @@ -26,12 +27,14 @@ 'security/ir.model.access.csv', 'data/project_data.xml', 'report/project_report_views.xml', + 'views/digest_views.xml', 'views/project_views.xml', 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', 'views/project_templates.xml', 'views/project_portal_templates.xml', 'data/web_planner_data.xml', + 'data/digest_data.xml', 'data/project_mail_template_data.xml', 'wizard/project_task_merge_wizard_views.xml', ], diff --git a/addons/project/data/digest_data.xml b/addons/project/data/digest_data.xml new file mode 100644 index 00000000..18947e35 --- /dev/null +++ b/addons/project/data/digest_data.xml @@ -0,0 +1,26 @@ + + + + + True + + + + + + 6 + + +
+ Try the mail gateway +
+ New tasks can be generated from incoming emails. You just need to set email aliases on your projects.
+ +
+
+
+
+
+
diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index 069e9802..86737692 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -6,3 +6,4 @@ from . import res_config_settings from . import res_company from . import res_partner from . import web_planner +from . import digest diff --git a/addons/project/models/digest.py b/addons/project/models/digest.py new file mode 100644 index 00000000..f6c271a5 --- /dev/null +++ b/addons/project/models/digest.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_project_task_opened = fields.Boolean('Open Tasks') + kpi_project_task_opened_value = fields.Integer(compute='_compute_project_task_opened_value') + + def _compute_project_task_opened_value(self): + if not self.env.user.has_group('project.group_project_user'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + record.kpi_project_task_opened_value = self.env['project.task'].search_count([ + ('stage_id.fold', '=', False), + ('create_date', '>=', start), + ('create_date', '<', end), + ('company_id', '=', company.id) + ]) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_project_task_opened'] = 'project.open_view_project_all&menu_id=%s' % self.env.ref('project.menu_main_pm').id + return res diff --git a/addons/project/views/digest_views.xml b/addons/project/views/digest_views.xml new file mode 100644 index 00000000..7b115317 --- /dev/null +++ b/addons/project/views/digest_views.xml @@ -0,0 +1,15 @@ + + + + digest.digest.view.form.inherit.project.task + digest.digest + + + + + + + + + + diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 80fe1939..4736b0ea 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -89,6 +89,10 @@ class ResourceCalendar(models.Model): 'resource.calendar.leaves', 'calendar_id', 'Global Leaves', domain=[('resource_id', '=', False)] ) + tz = fields.Selection( + _tz_get, string='Timezone', required=True, + default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC', + help="This field is used in order to define in which timezone the resources will work.") # -------------------------------------------------- # Utility methods diff --git a/addons/sale_expense/__manifest__.py b/addons/sale_expense/__manifest__.py index b41a0e07..a7687ae4 100644 --- a/addons/sale_expense/__manifest__.py +++ b/addons/sale_expense/__manifest__.py @@ -17,6 +17,7 @@ This module allow to reinvoice employee expense, by setting the SO directly on t 'website': 'https://flectrahq.com/page/warehouse', 'depends': ['sale_management', 'hr_expense'], 'data': [ + 'data/digest_data.xml', 'security/ir.model.access.csv', 'security/sale_expense_security.xml', 'views/product_view.xml', diff --git a/addons/sale_expense/data/digest_data.xml b/addons/sale_expense/data/digest_data.xml new file mode 100644 index 00000000..e17c1593 --- /dev/null +++ b/addons/sale_expense/data/digest_data.xml @@ -0,0 +1,17 @@ + + + + 5 + + +
+ Submit expenses by email +
Take a snapshot of your expenses and submit your expenses by email.
+ +
+
+
+
+
diff --git a/addons/sale_management/__init__.py b/addons/sale_management/__init__.py index 2cf7e628..008606b3 100644 --- a/addons/sale_management/__init__.py +++ b/addons/sale_management/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from . import models from flectra.api import Environment, SUPERUSER_ID diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py index 139282d6..5b40f4bf 100644 --- a/addons/sale_management/__manifest__.py +++ b/addons/sale_management/__manifest__.py @@ -42,10 +42,12 @@ The Dashboard for the Sales Manager will include * Monthly Turnover (Graph) """, 'website': 'https://flectrahq.com/page/sales', - 'depends': ['sale', 'account_invoicing'], + 'depends': ['sale', 'account_invoicing', 'digest'], 'data': [ + 'data/digest_data.xml', 'views/sale_management_views.xml', 'views/sale_management_templates.xml', + 'views/digest_views.xml', ], 'application': True, 'uninstall_hook': 'uninstall_hook', diff --git a/addons/sale_management/data/digest_data.xml b/addons/sale_management/data/digest_data.xml new file mode 100644 index 00000000..6e2c0970 --- /dev/null +++ b/addons/sale_management/data/digest_data.xml @@ -0,0 +1,6 @@ + + + + True + + diff --git a/addons/sale_management/models/__init__.py b/addons/sale_management/models/__init__.py new file mode 100644 index 00000000..104c0f2b --- /dev/null +++ b/addons/sale_management/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from . import digest diff --git a/addons/sale_management/models/digest.py b/addons/sale_management/models/digest.py new file mode 100644 index 00000000..6aa8192b --- /dev/null +++ b/addons/sale_management/models/digest.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_all_sale_total = fields.Boolean('All Sales') + kpi_all_sale_total_value = fields.Monetary(compute='_compute_kpi_sale_total_value') + + def _compute_kpi_sale_total_value(self): + if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + all_channels_sales = self.env['sale.report'].read_group([ + ('confirmation_date', '>=', start), + ('confirmation_date', '<', end), + ('company_id', '=', company.id)], ['price_total'], ['price_total']) + record.kpi_all_sale_total_value = sum([channel_sale['price_total'] for channel_sale in all_channels_sales]) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_all_sale_total'] = 'sale.report_all_channels_sales_action&menu_id=%s' % self.env.ref('sale.sale_menu_root').id + return res diff --git a/addons/sale_management/views/digest_views.xml b/addons/sale_management/views/digest_views.xml new file mode 100644 index 00000000..52a28455 --- /dev/null +++ b/addons/sale_management/views/digest_views.xml @@ -0,0 +1,17 @@ + + + + digest.digest.view.form.inherit.sale.order + digest.digest + + + + Sales + sales_team.group_sale_salesman_all_leads + + + + + + + diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index b88a1e5a..8f171e91 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -7,7 +7,7 @@ 'website': 'https://flectrahq.com/page/e-commerce', 'version': '1.1', 'description': "", - 'depends': ['website', 'sale_payment', 'website_payment', 'website_mail', 'website_form', 'website_rating'], + 'depends': ['website', 'sale_payment', 'website_payment', 'website_mail', 'website_form', 'website_rating', 'digest'], 'data': [ 'security/ir.model.access.csv', 'security/website_sale.xml', @@ -15,6 +15,7 @@ 'data/web_planner_data.xml', 'data/mail_template_data.xml', 'data/ir_cron_view.xml', + 'data/digest_data.xml', 'views/product_views.xml', 'views/account_views.xml', 'views/sale_report_views.xml', @@ -25,6 +26,7 @@ 'views/snippets.xml', 'views/report_shop_saleorder.xml', 'views/res_config_settings_views.xml', + 'views/digest_views.xml', ], 'demo': [ 'data/demo.xml', diff --git a/addons/website_sale/data/digest_data.xml b/addons/website_sale/data/digest_data.xml new file mode 100644 index 00000000..4f5ae3bd --- /dev/null +++ b/addons/website_sale/data/digest_data.xml @@ -0,0 +1,6 @@ + + + + True + + diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py index aed69db2..aea73053 100644 --- a/addons/website_sale/models/__init__.py +++ b/addons/website_sale/models/__init__.py @@ -14,3 +14,4 @@ from . import sale_report from . import ir_model_fields from . import website from . import res_config_settings +from . import digest diff --git a/addons/website_sale/models/digest.py b/addons/website_sale/models/digest.py new file mode 100644 index 00000000..933f3166 --- /dev/null +++ b/addons/website_sale/models/digest.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import fields, models, _ +from flectra.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_website_sale_total = fields.Boolean('eCommerce Sales') + kpi_website_sale_total_value = fields.Monetary(compute='_compute_kpi_website_sale_total_value') + + def _compute_kpi_website_sale_total_value(self): + if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + confirmed_website_sales = self.env['sale.order'].search([ + ('confirmation_date', '>=', start), + ('confirmation_date', '<', end), + ('state', 'not in', ['draft', 'cancel', 'sent']), + ('team_id.team_type', '=', 'website'), + ('company_id', '=', company.id) + ]) + record.kpi_website_sale_total_value = sum(confirmed_website_sales.mapped('amount_total')) + + def compute_kpis_actions(self, company, user): + res = super(Digest, self).compute_kpis_actions(company, user) + res['kpi_website_sale_total'] = 'website.backend_dashboard&menu_id=%s' % self.env.ref('website.menu_website_configuration').id + return res diff --git a/addons/website_sale/views/digest_views.xml b/addons/website_sale/views/digest_views.xml new file mode 100644 index 00000000..134f94f4 --- /dev/null +++ b/addons/website_sale/views/digest_views.xml @@ -0,0 +1,17 @@ + + + + digest.digest.view.form.inherit.website.sale.order + digest.digest + + + + Sales + sales_team.group_sale_salesman_all_leads + + + + + + + From 84ed2b2571417473fc312c4a0db0ab432dd70d3e Mon Sep 17 00:00:00 2001 From: Haresh Chavda Date: Fri, 24 Aug 2018 18:06:32 +0530 Subject: [PATCH 2/5] [ADD]: Add/Remove dynamic digest field, group and misc changes --- addons/digest/__manifest__.py | 4 +- addons/digest/views/digest_views.xml | 37 --------- addons/digest/views/digest_views_inherit.xml | 50 ++++++++++++ addons/digest/wizard/__init__.py | 3 +- addons/digest/wizard/digest_custom_fields.py | 76 +++++++++++-------- .../wizard/digest_custom_fields_view.xml | 8 +- addons/digest/wizard/digest_custom_remove.py | 76 +++++++++++++++++++ .../wizard/digest_custom_remove_view.xml | 31 ++++++++ addons/event/models/event.py | 4 +- 9 files changed, 215 insertions(+), 74 deletions(-) create mode 100644 addons/digest/views/digest_views_inherit.xml create mode 100644 addons/digest/wizard/digest_custom_remove.py create mode 100644 addons/digest/wizard/digest_custom_remove_view.xml diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py index 1541460e..fc2daee7 100644 --- a/addons/digest/__manifest__.py +++ b/addons/digest/__manifest__.py @@ -19,9 +19,11 @@ Send KPI Digests periodically 'data/ir_cron_data.xml', 'data/res_config_settings_data.xml', 'views/digest_views.xml', + 'wizard/digest_custom_fields_view.xml', + 'wizard/digest_custom_remove_view.xml', + 'views/digest_views_inherit.xml', 'views/digest_templates.xml', 'views/res_config_settings_views.xml', - 'wizard/digest_custom_fields_view.xml', ], 'installable': True, } diff --git a/addons/digest/views/digest_views.xml b/addons/digest/views/digest_views.xml index cf61541f..27f45967 100644 --- a/addons/digest/views/digest_views.xml +++ b/addons/digest/views/digest_views.xml @@ -62,43 +62,6 @@ - -
-
- -
diff --git a/addons/digest/views/digest_views_inherit.xml b/addons/digest/views/digest_views_inherit.xml new file mode 100644 index 00000000..a487680e --- /dev/null +++ b/addons/digest/views/digest_views_inherit.xml @@ -0,0 +1,50 @@ + + + + digest.digest.view.form.inherit + digest.digest + + + + +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/addons/digest/wizard/__init__.py b/addons/digest/wizard/__init__.py index a10b73b9..fad86bcd 100644 --- a/addons/digest/wizard/__init__.py +++ b/addons/digest/wizard/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Flectra. See LICENSE file for full copyright and licensing details. -from . import digest_custom_fields \ No newline at end of file +from . import digest_custom_fields +from . import digest_custom_remove \ No newline at end of file diff --git a/addons/digest/wizard/digest_custom_fields.py b/addons/digest/wizard/digest_custom_fields.py index b190636b..fcb6120c 100644 --- a/addons/digest/wizard/digest_custom_fields.py +++ b/addons/digest/wizard/digest_custom_fields.py @@ -5,6 +5,7 @@ from flectra import api, fields, models, _ from flectra.exceptions import ValidationError, UserError from lxml import etree from flectra.tools.safe_eval import test_python_expr +import xml.etree.ElementTree as ET class DigestCustomFields(models.TransientModel): @@ -19,29 +20,37 @@ class DigestCustomFields(models.TransientModel): # - time, datetime, dateutil, timezone: useful Python libraries # - log: log(message, level='info'): logging function to record debug information in ir.logging table # - Warning: Warning Exception to use with raise -# To return an action, assign: action = {...}\n\n\n\n""" +# To return an action, assign: action = {...} +for rec in self: + rec[''] = self.env[''].search([])\n\n\n\n""" - # field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '=', False)]") - # compute_field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '!=', False)]") - field_name = fields.Char('Field Name', default='x_kpi_', required=True) label_name = fields.Char('Label Name', required=True) - group_name = fields.Char('Group Name', required=True) - ttype = fields.Selection([('integer', 'Integer'), ('monetary', 'Monetary')], string='Field Type', required=True) - # compute = fields.Text(help="Code to compute the value of the field.\n" - # "Iterate on the recordset 'self' and assign the field's value:\n\n" - # " for record in self:\n" - # " record['size'] = len(record.name)\n\n" - # "Modules time, datetime, dateutil are available.") - - + # group_type = fields.Selection([('new', 'New Group'), ('existing', 'Existing Group')], string='Group Type', required=True) + new_group_name = fields.Char('Group Name') + ttype = fields.Selection([('integer', 'Integer'), ('monetary', 'Monetary')], string='Field Type', required=True, default='integer') compute = fields.Text(string='Python Code', groups='base.group_system', default=DEFAULT_PYTHON_CODE, help="Write Python code that the action will execute. Some variables are " "available for use; help about pyhon expression is given in the help tab.") compute_field_name = fields.Char(compute='_compute_get_field_name', string='Compute Field Name') + available_group_name = fields.Selection('_get_group_name', string='Available Group') + position = fields.Selection([('before', 'Before'), ('after', 'After'), ('inside', 'Inside')], string='Position') + + def _get_group_name(self): + print("=====self=========", self.env.context) + digest_view_id = self.env.ref('digest.digest_digest_view_form').id + view_ids = self.env['ir.ui.view'].search([('inherit_id', 'child_of', digest_view_id)]) + group_value = {} + for view_id in view_ids: + root = ET.fromstring(view_id.arch_base) + for group_name in root.iter('group'): + if group_name.attrib.get('name', False) and group_name.attrib.get('string', False): + group_key = str(view_id.id) + '_' + str(group_name.attrib['name']) + group_value.update({group_key : group_name.attrib['string']}) + return [(x) for x in group_value.items()] @api.constrains('compute') def _check_python_code(self): @@ -60,7 +69,9 @@ class DigestCustomFields(models.TransientModel): def _check_name(self): for field in self: if not field.field_name.startswith('x_kpi_'): - raise ValidationError(_("Custom fields must have a name that starts with 'x_kpi_' !")) + raise ValidationError(_("Custom fields must have a name that starts with 'x_kpi_'!")) + # if self.position != 'inside' and not field.new_group_name.startswith('x_kpi_'): + # raise ValidationError(_("Group Name must have a name that starts with 'x_kpi_'!")) try: models.check_pg_name(field.field_name) except ValidationError: @@ -90,41 +101,44 @@ class DigestCustomFields(models.TransientModel): 'depends': first_field_name, 'compute': self.compute } - print("====values=======", values) ir_model_fields_obj.create(values) def field_arch(self): xpath = etree.Element('xpath') - xpath_type = "group" - name = "kpi_general" - position = "after" - xpath_field = self.field_name - expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]' + name = self.available_group_name and self.available_group_name.split('_', 1)[1] or "kpis" + expr = '//' + 'group' + '[@name="' + name + '"]' xpath.set('expr', expr) - xpath.set('position', position) - if position == 'after' or position == 'before' or position == 'inside': - expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]' - group = etree.Element('group') - group.set('string', self.group_name) + xpath.set('position', self.position) + + if self.position == 'inside': field = etree.Element('field') - field.set('name', xpath_field) + field.set('name', self.field_name) + xpath.set('expr', expr) + xpath.append(field) + else: + group = etree.Element('group') + group.set('name', 'x_kpi_' + self.new_group_name.replace(" ", "_")) + group.set('string', self.new_group_name) + field = etree.Element('field') + field.set('name', self.field_name) xpath.set('expr', expr) group.append(field) xpath.append(group) + return etree.tostring(xpath).decode("utf-8") @api.multi - def action_customize_digest(self): + def action_add_customize_digest(self): self.add_new_fields() - arch = '' + str(self.field_arch()) + '' - print("====arch=======", arch) + arch = '' + str(self.field_arch()) + view_id = self.available_group_name and self.available_group_name.split('_', 1)[0] or False vals = { 'type': 'form', 'model': 'digest.digest', - 'inherit_id': self.env.ref('digest.digest_digest_view_form').id, + 'inherit_id': view_id or self.env.ref('digest.digest_digest_view_form').id, 'mode': 'extension', 'arch_base': arch, - 'name': 'x_kpi_' + self.field_name + "_Customization", + 'name': 'x_kpi_' + self.field_name + "_customization", } ir_model = self.env['ir.model'].search([('model', '=', 'digest.digest')]) if hasattr(ir_model, 'module_id'): diff --git a/addons/digest/wizard/digest_custom_fields_view.xml b/addons/digest/wizard/digest_custom_fields_view.xml index 732608c0..c66c435a 100644 --- a/addons/digest/wizard/digest_custom_fields_view.xml +++ b/addons/digest/wizard/digest_custom_fields_view.xml @@ -5,10 +5,12 @@ digest.custom.fields
- + - + + + @@ -16,7 +18,7 @@
-
diff --git a/addons/digest/wizard/digest_custom_remove.py b/addons/digest/wizard/digest_custom_remove.py new file mode 100644 index 00000000..d8a6344a --- /dev/null +++ b/addons/digest/wizard/digest_custom_remove.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, fields, models, _ +from flectra.exceptions import ValidationError, UserError +from lxml import etree +from flectra.tools.safe_eval import test_python_expr +import xml.etree.ElementTree as ET +from flectra.osv import expression + + +class DigestCustomRemove(models.TransientModel): + _name = 'digest.custom.remove' + + remove_type = fields.Selection([('group', 'Group'), ('field', 'Field')], string='Remove Type') + field_id = fields.Many2one('ir.model.fields', 'Field', domain=[('model', '=', 'digest.digest'), ('required', '=', False), ('ttype', '=', 'boolean'), ('name', 'ilike', 'x_kpi_')]) + available_group_name = fields.Selection('_get_group_name', string='Available Group') + + def _get_group_name(self): + digest_view_id = self.env.ref('digest.digest_digest_view_form').id + view_ids = self.env['ir.ui.view'].search([('inherit_id', 'child_of', digest_view_id)]) + group_value = {} + for view_id in view_ids: + root = ET.fromstring(view_id.arch_base) + for group_name in root.iter('group'): + if group_name.attrib.get('name', False) and group_name.attrib.get('string', False) and group_name.attrib['name'].startswith('x_kpi_'): + group_key = str(view_id.id) + '_' + str(group_name.attrib['name']) + group_value.update({group_key : group_name.attrib['string']}) + return [(x) for x in group_value.items()] + + @api.multi + def action_customize_digest_remove(self): + ir_model_fields_obj = self.env['ir.model.fields'] + ir_ui_view_obj = self.env['ir.ui.view'] + if self.remove_type == 'group': + find_view_id = self.available_group_name and self.available_group_name.split('_', 1)[0] or False + print("===find_view_id==", find_view_id) + view_ids = ir_ui_view_obj.search([('inherit_id', 'child_of', int(find_view_id))], order="id desc") + field_list = [] + for view_id in view_ids: + print("===view_id========", view_id) + root = ET.fromstring(view_id.arch_base) + print("==========root=====", root) + for child in root.iter('group'): + name = child.find('field') + if name.attrib and name.attrib.get('name', False): + field_list.append(name.attrib.get('name', False)) + field_ids = ir_model_fields_obj.search([('name', 'in', field_list)]) + print("====field_ids====", field_ids, view_ids.ids) + view_ids.unlink() + for field_id in field_ids: + ir_model_fields_obj.search([('depends', '=', field_id.name)]).unlink() + field_ids.unlink() + else: + domain = expression.OR([('arch_db', 'like', record.name)] for record in self.field_id) + print("===domain======", domain) + view_ids = ir_ui_view_obj.search(domain) + print("==========>>>>>>>>.", view_ids) + for view_id in view_ids: + # print("=====view_id.arch_base======before========", view_id.arch_base) + root = ET.fromstring(view_id.arch_base) + # result = len(root.getchildren()) + # count = sum(1 for root in root.iter("field")) + # print("===============result==============>", result, count) + for child in root.iter('field'): + # print("===========>>>>",child.text, child.attrib, child.tag) + if child.attrib and child.attrib.get('name', False) == self.field_id.name: + view_id.unlink() + # root.remove(child) + # view_id.write({'arch_base': ET.tostring(root)}) + ir_model_fields_obj.search([('depends', '=', self.field_id.name)]).unlink() + self.field_id.unlink() + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } \ No newline at end of file diff --git a/addons/digest/wizard/digest_custom_remove_view.xml b/addons/digest/wizard/digest_custom_remove_view.xml new file mode 100644 index 00000000..ae3d027b --- /dev/null +++ b/addons/digest/wizard/digest_custom_remove_view.xml @@ -0,0 +1,31 @@ + + + + digest.custom.remove.form + digest.custom.remove + +
+ + + + + + + +
+
+
+
+
+ + + Customized Digest Remove + ir.actions.act_window + digest.custom.remove + form + + new + +
diff --git a/addons/event/models/event.py b/addons/event/models/event.py index a5b2f737..a27e0db5 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -202,7 +202,9 @@ class EventEvent(models.Model): @api.model def _tz_get(self): - return [(x, x) for x in pytz.all_timezones] + a = [(x, x) for x in pytz.all_timezones] + print("\n\n\n=================>>>>>>.", a) + return a @api.one @api.depends('date_tz', 'date_begin') From c2fd95a87b2061d4195afa2eda4fa14a4f6477d4 Mon Sep 17 00:00:00 2001 From: Haresh Chavda Date: Fri, 31 Aug 2018 17:39:21 +0530 Subject: [PATCH 3/5] [IMP]: dynamic set domain, field and model for new digest field --- addons/digest/__manifest__.py | 2 +- addons/digest/data/digest_template_data.xml | 30 +------- addons/digest/models/digest.py | 6 +- addons/digest/models/res_users.py | 5 +- addons/digest/views/digest_templates.xml | 29 ++++++++ addons/digest/views/digest_views.xml | 5 ++ addons/digest/wizard/digest_custom_fields.py | 72 ++++++++++--------- .../wizard/digest_custom_fields_view.xml | 35 +++++++-- addons/digest/wizard/digest_custom_remove.py | 13 ---- .../wizard/digest_custom_remove_view.xml | 13 ++-- 10 files changed, 122 insertions(+), 88 deletions(-) diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py index fc2daee7..603799a8 100644 --- a/addons/digest/__manifest__.py +++ b/addons/digest/__manifest__.py @@ -15,6 +15,7 @@ Send KPI Digests periodically 'data': [ 'security/ir.model.access.csv', 'data/digest_template_data.xml', + 'views/digest_templates.xml', 'data/digest_data.xml', 'data/ir_cron_data.xml', 'data/res_config_settings_data.xml', @@ -22,7 +23,6 @@ Send KPI Digests periodically 'wizard/digest_custom_fields_view.xml', 'wizard/digest_custom_remove_view.xml', 'views/digest_views_inherit.xml', - 'views/digest_templates.xml', 'views/res_config_settings_views.xml', ], 'installable': True, diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml index 0f41eae5..4076410d 100644 --- a/addons/digest/data/digest_template_data.xml +++ b/addons/digest/data/digest_template_data.xml @@ -10,8 +10,8 @@
- % set company, user = ctx['company'], ctx['user'] - % set data = object.compute_kpis(company, user) + % set company, user = user.company_id, user + % set data = object.compute_kpis(user.company, user) % set tips = object.compute_tips(company, user) % set kpi_actions = object.compute_kpis_actions(company, user) % set kpis = data.yesterday.keys() @@ -120,32 +120,6 @@
% endif - - - - - - - -

-
Run your bussiness from anywhere with Flectra Mobile.
-
-
-
-
- - - - -
- % if ctx['user'].has_group('base.group_system'): -
- Want to customize the email? - Choose the metrics you care about -
-
- % endif -
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index d46c5eca..a7723a86 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -44,7 +44,6 @@ class Digest(models.Model): kpi_mail_message_total = fields.Boolean('Messages') kpi_mail_message_total_value = fields.Integer(compute='_compute_kpi_mail_message_total_value') - def _compute_is_subscribed(self): for digest in self: digest.is_subscribed = self.env.user in digest.user_ids @@ -108,6 +107,10 @@ class Digest(models.Model): def compute_kpis(self, company, user): self.ensure_one() + if not company: + company = self.env.user.company_id + if not user: + user = self.env.user res = {} for tf_name, tf in self._compute_timeframes(company).items(): digest = self.with_context(start_date=tf[0][0], end_date=tf[0][1], company=company).sudo(user.id) @@ -115,7 +118,6 @@ class Digest(models.Model): kpis = {} for field_name, field in self._fields.items(): if field.type == 'boolean' and (field_name.startswith('kpi_') or field_name.startswith('x_kpi_')) and self[field_name]: - try: compute_value = digest[field_name + '_value'] previous_value = previous_digest[field_name + '_value'] diff --git a/addons/digest/models/res_users.py b/addons/digest/models/res_users.py index 34e12593..984ff493 100644 --- a/addons/digest/models/res_users.py +++ b/addons/digest/models/res_users.py @@ -10,8 +10,9 @@ class ResUsers(models.Model): def create(self, vals): """ Automatically subscribe employee users to default digest if activated """ user = super(ResUsers, self).create(vals) - default_digest_emails = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_emails') - default_digest_id = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_id') + config_obj = self.env['ir.config_parameter'].sudo() + default_digest_emails = config_obj.get_param('digest.default_digest_emails') + default_digest_id = config_obj.get_param('digest.default_digest_id') if user.has_group('base.group_user') and default_digest_emails and default_digest_id: digest = self.env['digest.digest'].sudo().browse(int(default_digest_id)) digest.user_ids |= user diff --git a/addons/digest/views/digest_templates.xml b/addons/digest/views/digest_templates.xml index ae3742f4..ee27e581 100644 --- a/addons/digest/views/digest_templates.xml +++ b/addons/digest/views/digest_templates.xml @@ -13,4 +13,33 @@ + + + email_template.preview.digest.form + email_template.preview + +
+ Choose an example record: + + +
+
+ +
+
+ + + Preview of Digest + email_template.preview + digest.digest + ir.actions.act_window + form + form + + new + {'template_id':template_id, 'default_res_id': active_id} + + \ No newline at end of file diff --git a/addons/digest/views/digest_views.xml b/addons/digest/views/digest_views.xml index 27f45967..88f55f0a 100644 --- a/addons/digest/views/digest_views.xml +++ b/addons/digest/views/digest_views.xml @@ -1,5 +1,6 @@ + digest.digest.view.tree digest.digest @@ -36,6 +37,9 @@ +
+