diff --git a/addons/account/account.py b/addons/account/account.py
index acf27c42392..8c7f058acc6 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -1150,6 +1150,29 @@ class account_move(osv.osv):
_description = "Account Entry"
_order = 'id desc'
+ def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None):
+ '''
+ Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received.
+ '''
+ if not date:
+ date = fields.date.today()
+ period_obj = self.pool.get('account.period')
+ if not company_id:
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ company_id = user.company_id.id
+ if context is None:
+ context = {}
+ #put the company in context to find the good period
+ ctx = context.copy()
+ ctx.update({'company_id': company_id})
+ return {
+ 'journal_id': journal_id,
+ 'date': date,
+ 'period_id': period_obj.find(cr, uid, date, context=ctx)[0],
+ 'ref': ref,
+ 'company_id': company_id,
+ }
+
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
"""
Returns a list of tupples containing id, name, as internally it is called {def name_get}
@@ -1850,6 +1873,13 @@ class account_tax(osv.osv):
return result in the context
Ex: result=round(price_unit*0.21,4)
"""
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ name = self.read(cr, uid, id, ['name'], context=context)['name']
+ default = default.copy()
+ default.update({'name': name + _(' (Copy)')})
+ return super(account_tax, self).copy_data(cr, uid, id, default=default, context=context)
def get_precision_tax():
def change_digit_tax(cr):
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index dbe17d1c60a..d771bc59506 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -22,6 +22,7 @@
import time
from lxml import etree
import openerp.addons.decimal_precision as dp
+import openerp.exceptions
from openerp import pooler
from openerp.osv import fields, osv, orm
@@ -302,16 +303,7 @@ class account_invoice(osv.osv):
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
]
- def _find_partner(self, inv):
- '''
- Find the partner for which the accounting entries will be created
- '''
- #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
- #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
- part = inv.partner_id
- if part.parent_id and not part.is_company:
- part = part.parent_id
- return part
+
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
@@ -981,7 +973,7 @@ class account_invoice(osv.osv):
date = inv.date_invoice or time.strftime('%Y-%m-%d')
- part = self._find_partner(inv)
+ part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id)
line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml)
@@ -1753,6 +1745,16 @@ class res_partner(osv.osv):
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
}
+ def _find_accounting_partner(self, part):
+ '''
+ Find the partner for which the accounting entries will be created
+ '''
+ #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
+ #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
+ if part.parent_id and not part.is_company:
+ part = part.parent_id
+ return part
+
def copy(self, cr, uid, id, default=None, context=None):
default = default or {}
default.update({'invoice_ids' : []})
diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml
index a37474db0af..8c0854bd6c6 100644
--- a/addons/account/account_invoice_view.xml
+++ b/addons/account/account_invoice_view.xml
@@ -145,8 +145,7 @@
-
-
+
@@ -300,7 +299,7 @@
-
+
@@ -437,7 +436,7 @@
-
+
@@ -448,11 +447,11 @@
account.invoice
-
-
-
-
-
+
+
+
+
+
diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py
index f867688f179..a96ed889aae 100644
--- a/addons/account/account_move_line.py
+++ b/addons/account/account_move_line.py
@@ -780,7 +780,7 @@ class account_move_line(osv.osv):
else:
currency_id = line.company_id.currency_id
if line.reconcile_id:
- raise osv.except_osv(_('Warning!'), _('Already reconciled.'))
+ raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name))
if line.reconcile_partial_id:
for line2 in line.reconcile_partial_id.line_partial_ids:
if not line2.reconcile_id:
diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index b4260db6b90..b8dcefdd3a4 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -161,7 +161,7 @@
+ When reinvoicing costs, OpenERP uses the
pricelist of the contract which uses the price
- defined on the product related to each employee to
- define the customer invoice price rate.
+ defined on the product related (e.g timesheet
+ products are defined on each employee).
-
+
-
+
diff --git a/addons/account_cancel/account_cancel_view.xml b/addons/account_cancel/account_cancel_view.xml
index 4ed8093a0aa..37e9dcd1ee8 100644
--- a/addons/account_cancel/account_cancel_view.xml
+++ b/addons/account_cancel/account_cancel_view.xml
@@ -18,7 +18,7 @@
-
+
@@ -29,7 +29,7 @@
-
+
diff --git a/addons/account_followup/account_followup.py b/addons/account_followup/account_followup.py
index a4e8f874332..036feaaf0e5 100644
--- a/addons/account_followup/account_followup.py
+++ b/addons/account_followup/account_followup.py
@@ -292,8 +292,7 @@ class res_partner(osv.osv):
type = 'comment',
subtype = "mail.mt_comment", context = context,
model = 'res.partner', res_id = part.id,
- notified_partner_ids = [(6, 0, [responsible_partner_id])],
- partner_ids = [(6, 0, [responsible_partner_id])])
+ partner_ids = [responsible_partner_id])
return super(res_partner, self).write(cr, uid, ids, vals, context=context)
def action_done(self, cr, uid, ids, context=None):
diff --git a/addons/account_followup/account_followup_customers.xml b/addons/account_followup/account_followup_customers.xml
index 78a9c892614..7a3808ddefb 100644
--- a/addons/account_followup/account_followup_customers.xml
+++ b/addons/account_followup/account_followup_customers.xml
@@ -24,6 +24,17 @@
+
+ res.partner.followup.inherit.tree
+ res.partner
+
+
+
+
+
+
+
+
Searchres.partner
diff --git a/addons/account_followup/tests/test_account_followup.py b/addons/account_followup/tests/test_account_followup.py
index 8ddf3f82288..a421931665a 100644
--- a/addons/account_followup/tests/test_account_followup.py
+++ b/addons/account_followup/tests/test_account_followup.py
@@ -52,10 +52,10 @@ class TestAccountFollowup(TransactionCase):
def test_00_send_followup_after_3_days(self):
""" Send follow up after 3 days and check nothing is done (as first follow-up level is only after 15 days)"""
cr, uid = self.cr, self.uid
- current_date = datetime.datetime.now()
+ current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=3)
result = current_date + delta
- self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"),
+ self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id
}, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
@@ -64,34 +64,33 @@ class TestAccountFollowup(TransactionCase):
def run_wizard_three_times(self):
cr, uid = self.cr, self.uid
- current_date = datetime.datetime.now()
+ current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=40)
result = current_date + delta
- self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"),
+ self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id
}, context={"followup_id": self.followup_id})
- self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
- self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"),
+ self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
+ self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id
}, context={"followup_id": self.followup_id})
- self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
- self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"),
- 'followup_id': self.followup_id
+ self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
+ self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
+ 'followup_id': self.followup_id,
}, context={"followup_id": self.followup_id})
- self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
-
+ self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
def test_01_send_followup_later_for_upgrade(self):
""" Send one follow-up after 15 days to check it upgrades to level 1"""
cr, uid = self.cr, self.uid
- current_date = datetime.datetime.now()
+ current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=15)
result = current_date + delta
self.wizard_id = self.wizard.create(cr, uid, {
- 'date':result.strftime("%Y-%m-%d"),
+ 'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id
}, context={"followup_id": self.followup_id})
- self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
+ self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id.id, self.first_followup_line_id,
"Not updated to the correct follow-up level")
@@ -102,12 +101,12 @@ class TestAccountFollowup(TransactionCase):
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action,
"Call the customer on the phone! ", "Manual action not set")
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action_date,
- datetime.datetime.now().strftime("%Y-%m-%d"))
-
+ datetime.datetime.utcnow().strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
+
def test_03_filter_on_credit(self):
""" Check the partners can be filtered on having credits """
cr, uid = self.cr, self.uid
- ids = self.partner.search(cr, uid, [('payment_amount_due', '>=', 0.0)])
+ ids = self.partner.search(cr, uid, [('payment_amount_due', '>', 0.0)])
self.assertIn(self.partner_id, ids)
def test_04_action_done(self):
@@ -139,7 +138,7 @@ class TestAccountFollowup(TransactionCase):
"""Run wizard until manual action, pay the invoice and check that partner has no follow-up level anymore and after running the wizard the action is empty"""
cr, uid = self.cr, self.uid
self.test_02_check_manual_action()
- current_date = datetime.datetime.now()
+ current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=1)
result = current_date + delta
self.invoice.pay_and_reconcile(cr, uid, [self.invoice_id], 1000.0, self.pay_account_id,
@@ -147,7 +146,7 @@ class TestAccountFollowup(TransactionCase):
self.period_id, self.journal_id,
name = "Payment for test customer invoice follow-up")
self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id, "Level not empty")
- self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"),
+ self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id
}, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
diff --git a/addons/account_payment/account_payment_view.xml b/addons/account_payment/account_payment_view.xml
index e07043c495a..c2f27a576a2 100644
--- a/addons/account_payment/account_payment_view.xml
+++ b/addons/account_payment/account_payment_view.xml
@@ -88,7 +88,7 @@
-
+
diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml
index 1e1481c3f2b..f429480f656 100644
--- a/addons/account_voucher/account_voucher_view.xml
+++ b/addons/account_voucher/account_voucher_view.xml
@@ -43,8 +43,8 @@
diff --git a/addons/auth_signup/__openerp__.py b/addons/auth_signup/__openerp__.py
index 4e1ada7bd19..08740962c59 100644
--- a/addons/auth_signup/__openerp__.py
+++ b/addons/auth_signup/__openerp__.py
@@ -42,4 +42,5 @@ Allow users to sign up and reset their password
],
'js': ['static/src/js/auth_signup.js'],
'qweb': ['static/src/xml/auth_signup.xml'],
+ 'bootstrap': True,
}
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index baf1a8f0b28..924e1546862 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -19,9 +19,6 @@
#
##############################################################################
import logging
-import urllib
-
-import werkzeug
import openerp
from openerp.modules.registry import RegistryManager
@@ -54,9 +51,8 @@ class Controller(openerp.addons.web.http.Controller):
return user_info
@openerp.addons.web.http.jsonrequest
- def signup(self, req, dbname, token, name, login, password):
+ def signup(self, req, dbname, token, **values):
""" sign up a user (new or existing)"""
- values = {'name': name, 'login': login, 'password': password}
try:
self._signup_with_values(req, dbname, token, values)
except SignupError, e:
@@ -69,7 +65,7 @@ class Controller(openerp.addons.web.http.Controller):
res_users = registry.get('res.users')
res_users.signup(cr, openerp.SUPERUSER_ID, values, token)
- @openerp.addons.web.http.httprequest
+ @openerp.addons.web.http.jsonrequest
def reset_password(self, req, dbname, login):
""" retrieve user, and perform reset password """
registry = RegistryManager.get(dbname)
@@ -78,12 +74,10 @@ class Controller(openerp.addons.web.http.Controller):
res_users = registry.get('res.users')
res_users.reset_password(cr, openerp.SUPERUSER_ID, login)
cr.commit()
- message = 'An email has been sent with credentials to reset your password'
except Exception as e:
# signup error
_logger.exception('error when resetting password')
- message = e.message
- params = [('action', 'login'), ('error_message', message)]
- return werkzeug.utils.redirect("/#" + urllib.urlencode(params))
+ raise(e)
+ return True
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/auth_signup/static/src/js/auth_signup.js b/addons/auth_signup/static/src/js/auth_signup.js
index 6757b9cb69d..fe071fde55d 100644
--- a/addons/auth_signup/static/src/js/auth_signup.js
+++ b/addons/auth_signup/static/src/js/auth_signup.js
@@ -5,32 +5,33 @@ openerp.auth_signup = function(instance) {
instance.web.Login.include({
start: function() {
var self = this;
- var d = this._super();
- d.done(function() {
- self.$(".oe_signup_show").hide();
+ this.signup_enabled = false;
+ this.reset_password_enabled = false;
+ return this._super().then(function() {
+
+ // Switches the login box to the select mode whith mode == [default|signup|reset]
+ self.on('change:login_mode', self, function() {
+ var mode = self.get('login_mode') || 'default';
+ self.$('*[data-modes]').each(function() {
+ var modes = $(this).data('modes').split(/\s+/);
+ $(this).toggle(modes.indexOf(mode) > -1);
+ });
+ self.$('a.oe_signup_signup:visible').toggle(self.signup_enabled);
+ self.$('a.oe_signup_reset_password:visible').toggle(self.reset_password_enabled);
+ });
+
// to switch between the signup and regular login form
self.$('a.oe_signup_signup').click(function(ev) {
- if (ev) {
- ev.preventDefault();
- }
- self.$el.addClass("oe_login_signup");
- self.$(".oe_signup_show").show();
- self.$(".oe_signup_hide").hide();
+ self.set('login_mode', 'signup');
return false;
});
self.$('a.oe_signup_back').click(function(ev) {
- if (ev) {
- ev.preventDefault();
- }
- self.$el.removeClass("oe_login_signup");
- self.$(".oe_signup_show").hide();
- self.$(".oe_signup_hide").show();
+ self.set('login_mode', 'default');
delete self.params.token;
return false;
});
- var dblist = self.db_list || [];
- var dbname = self.params.db || (dblist.length === 1 ? dblist[0] : null);
+ var dbname = self.selected_db;
// if there is an error message in params, show it then forget it
if (self.params.error_message) {
@@ -42,7 +43,7 @@ openerp.auth_signup = function(instance) {
if (dbname && self.params.token) {
self.rpc("/auth_signup/retrieve", {dbname: dbname, token: self.params.token})
.done(self.on_token_loaded)
- .fail(self.on_token_failed)
+ .fail(self.on_token_failed);
}
if (dbname && self.params.login) {
self.$("form input[name=login]").val(self.params.login);
@@ -51,23 +52,21 @@ openerp.auth_signup = function(instance) {
// bind reset password link
self.$('a.oe_signup_reset_password').click(self.do_reset_password);
- // make signup link and reset password link visible only when enabled
- self.$('a.oe_signup_signup').hide();
- self.$('a.oe_signup_reset_password').hide();
if (dbname) {
- self.rpc("/auth_signup/get_config", {dbname: dbname})
- .done(function(result) {
- if (result.signup) {
- self.$('a.oe_signup_signup').show();
- }
- if (result.reset_password) {
- self.$('a.oe_signup_reset_password').show();
- }
- });
+ self.rpc("/auth_signup/get_config", {dbname: dbname}).done(function(result) {
+ self.signup_enabled = result.signup;
+ self.reset_password_enabled = result.reset_password;
+ if (self.$("form input[name=login]").val()){
+ self.set('login_mode', 'default');
+ } else {
+ self.set('login_mode', 'signup');
+ }
+ });
+ } else {
+ // TODO: support multiple database mode
+ self.set('login_mode', 'default');
}
});
-
- return d;
},
on_token_loaded: function(result) {
@@ -76,9 +75,7 @@ openerp.auth_signup = function(instance) {
this.on_db_loaded([result.db]);
if (result.token) {
// switch to signup mode, set user name and login
- this.$el.addClass("oe_login_signup");
- self.$(".oe_signup_show").show();
- self.$(".oe_signup_hide").hide();
+ this.set('login_mode', (this.params.type === 'reset' ? 'reset' : 'signup'));
this.$("form input[name=name]").val(result.name).attr("readonly", "readonly");
if (result.login) {
this.$("form input[name=login]").val(result.login).attr("readonly", "readonly");
@@ -88,6 +85,7 @@ openerp.auth_signup = function(instance) {
} else {
// remain in login mode, set login if present
delete this.params.token;
+ this.set('login_mode', 'default');
this.$("form input[name=login]").val(result.login || "");
}
},
@@ -99,43 +97,52 @@ openerp.auth_signup = function(instance) {
this.show_error(_t("Invalid signup token"));
delete this.params.db;
delete this.params.token;
+ this.set('login_mode', 'default');
+ },
+
+ get_params: function(){
+ // signup user (or reset password)
+ var db = this.$("form [name=db]").val();
+ var name = this.$("form input[name=name]").val();
+ var login = this.$("form input[name=login]").val();
+ var password = this.$("form input[name=password]").val();
+ var confirm_password = this.$("form input[name=confirm_password]").val();
+ if (!db) {
+ this.do_warn(_t("Login"), _t("No database selected !"));
+ return false;
+ } else if (!name) {
+ this.do_warn(_t("Login"), _t("Please enter a name."));
+ return false;
+ } else if (!login) {
+ this.do_warn(_t("Login"), _t("Please enter a username."));
+ return false;
+ } else if (!password || !confirm_password) {
+ this.do_warn(_t("Login"), _t("Please enter a password and confirm it."));
+ return false;
+ } else if (password !== confirm_password) {
+ this.do_warn(_t("Login"), _t("Passwords do not match; please retype them."));
+ return false;
+ }
+ var params = {
+ dbname : db,
+ token: this.params.token || "",
+ name: name,
+ login: login,
+ password: password,
+ };
+ return params;
},
on_submit: function(ev) {
if (ev) {
ev.preventDefault();
}
- if (this.$el.hasClass("oe_login_signup")) {
- // signup user (or reset password)
- var db = this.$("form [name=db]").val();
- var name = this.$("form input[name=name]").val();
- var login = this.$("form input[name=login]").val();
- var password = this.$("form input[name=password]").val();
- var confirm_password = this.$("form input[name=confirm_password]").val();
- if (!db) {
- this.do_warn(_t("Login"), _t("No database selected !"));
- return false;
- } else if (!name) {
- this.do_warn(_t("Login"), _t("Please enter a name."));
- return false;
- } else if (!login) {
- this.do_warn(_t("Login"), _t("Please enter a username."));
- return false;
- } else if (!password || !confirm_password) {
- this.do_warn(_t("Login"), _t("Please enter a password and confirm it."));
- return false;
- } else if (password !== confirm_password) {
- this.do_warn(_t("Login"), _t("Passwords do not match; please retype them."));
+ var login_mode = this.get('login_mode');
+ if (login_mode === 'signup' || login_mode === 'reset') {
+ var params = this.get_params();
+ if (_.isEmpty(params)){
return false;
}
- var params = {
- dbname : db,
- token: this.params.token || "",
- name: name,
- login: login,
- password: password,
- };
-
var self = this,
super_ = this._super;
this.rpc('/auth_signup/signup', params)
@@ -156,21 +163,23 @@ openerp.auth_signup = function(instance) {
if (ev) {
ev.preventDefault();
}
+ var self = this;
var db = this.$("form [name=db]").val();
var login = this.$("form input[name=login]").val();
if (!db) {
this.do_warn(_t("Login"), _t("No database selected !"));
- return false;
+ return $.Deferred().reject();
} else if (!login) {
- this.do_warn(_t("Login"), _t("Please enter a username or email address."))
- return false;
+ this.do_warn(_t("Login"), _t("Please enter a username or email address."));
+ return $.Deferred().reject();
}
- var params = {
- dbname : db,
- login: login,
- };
- var url = "/auth_signup/reset_password?" + $.param(params);
- window.location = url;
+ return self.rpc("/auth_signup/reset_password", { dbname: db, login: login }).done(function(result) {
+ self.show_error(_t("An email has been sent with credentials to reset your password"));
+ self.set('login_mode', 'default');
+ }).fail(function(result, ev) {
+ ev.preventDefault();
+ self.show_error(result.message);
+ });
},
});
};
diff --git a/addons/auth_signup/static/src/xml/auth_signup.xml b/addons/auth_signup/static/src/xml/auth_signup.xml
index d1c04cb6807..9ef4f394e27 100644
--- a/addons/auth_signup/static/src/xml/auth_signup.xml
+++ b/addons/auth_signup/static/src/xml/auth_signup.xml
@@ -5,26 +5,32 @@
-
diff --git a/addons/lunch/lunch_demo.xml b/addons/lunch/lunch_demo.xml
index 7488208fd42..7e5e2f3ac8f 100644
--- a/addons/lunch/lunch_demo.xml
+++ b/addons/lunch/lunch_demo.xml
@@ -2,9 +2,6 @@
-
-
-
diff --git a/addons/lunch/lunch_view.xml b/addons/lunch/lunch_view.xml
index c975b766ccd..2d662a2f785 100644
--- a/addons/lunch/lunch_view.xml
+++ b/addons/lunch/lunch_view.xml
@@ -283,7 +283,7 @@
Order lines Treelunch.order.line
-
+
diff --git a/addons/lunch/security/lunch_security.xml b/addons/lunch/security/lunch_security.xml
index 654f844c876..e01fab71ea2 100644
--- a/addons/lunch/security/lunch_security.xml
+++ b/addons/lunch/security/lunch_security.xml
@@ -14,6 +14,7 @@
Manager
+
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index a9d33f4585d..ecdd39874e3 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -75,20 +75,25 @@ class mail_notification(osv.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
- def get_partners_to_notify(self, cr, uid, message, context=None):
+ def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
+ :param list partners_to_notify: optional list of partner ids restricting
+ the notifications to process
"""
notify_pids = []
for notification in message.notification_ids:
if notification.read:
continue
partner = notification.partner_id
+ # If partners_to_notify specified: restrict to them
+ if partners_to_notify and partner.id not in partners_to_notify:
+ continue
# Do not send to partners without email address defined
if not partner.email:
continue
- # Partner does not want to receive any emails
+ # Partner does not want to receive any emails or is opt-out
if partner.notification_email_send == 'none':
continue
# Partner wants to receive only emails and comments
@@ -100,16 +105,35 @@ class mail_notification(osv.Model):
notify_pids.append(partner.id)
return notify_pids
- def _notify(self, cr, uid, msg_id, context=None):
- """ Send by email the notification depending on the user preferences """
+ def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None):
+ """ Send by email the notification depending on the user preferences
+
+ :param list partners_to_notify: optional list of partner ids restricting
+ the notifications to process
+ """
if context is None:
context = {}
+ mail_message_obj = self.pool.get('mail.message')
+
+ # optional list of partners to notify: subscribe them if not already done or update the notification
+ if partners_to_notify:
+ notifications_to_update = []
+ notified_partners = []
+ notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
+ for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
+ notified_partners.append(notification.partner_id.id)
+ notifications_to_update.append(notification.id)
+ partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
+ if notifications_to_update:
+ self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
+ mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
+
# mail_notify_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_notify_noemail'):
return True
# browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
- notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context)
+ notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
if not notify_partner_ids:
return True
@@ -136,16 +160,12 @@ class mail_notification(osv.Model):
mail_values = {
'mail_message_id': msg.id,
- 'email_to': [],
'auto_delete': True,
'body_html': body_html,
'email_from': email_from,
- 'state': 'outgoing',
}
- mail_values['email_to'] = ', '.join(mail_values['email_to'])
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
try:
return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)
except Exception:
return False
-
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index fba192b18bc..77c0b8f6c86 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -106,7 +106,7 @@
-
+
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 1b3ca152a9a..8bca0f85b94 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -191,20 +191,31 @@ class mail_mail(osv.Model):
return body
def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
- """ Return a specific ir_email body. The main purpose of this method
- is to be inherited by Portal, to add a link for signing in, in
- each notification email a partner receives.
+ """ Return a specific ir_email reply_to.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
if mail.reply_to:
return mail.reply_to
- if not mail.model or not mail.res_id:
- return False
- if not hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
- return False
- return self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
+ email_reply_to = False
+
+ # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
+ if mail.model and mail.res_id and hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
+ email_reply_to = self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
+ # no alias reply_to -> reply_to will be the email_from, only the email part
+ if not email_reply_to and mail.email_from:
+ emails = tools.email_split(mail.email_from)
+ if emails:
+ email_reply_to = emails[0]
+
+ # format 'Document name '
+ if email_reply_to and mail.model and mail.res_id:
+ document_name = self.pool.get(mail.model).name_get(cr, SUPERUSER_ID, [mail.res_id], context=context)[0]
+ if document_name:
+ email_reply_to = _('Followers of %s <%s>') % (document_name[1], email_reply_to)
+
+ return email_reply_to
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
index 72d41e33a84..fb2b66adb36 100644
--- a/addons/mail/mail_mail_view.xml
+++ b/addons/mail/mail_mail_view.xml
@@ -75,7 +75,7 @@
-
+
@@ -102,11 +102,10 @@
-
-
-
-
-
+
+
+
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 77573f0d2f8..2f56e620543 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -301,8 +301,8 @@ class mail_message(osv.Model):
for key, message in message_tree.iteritems():
if message.author_id:
partner_ids |= set([message.author_id.id])
- if message.partner_ids:
- partner_ids |= set([partner.id for partner in message.partner_ids])
+ if message.notified_partner_ids:
+ partner_ids |= set([partner.id for partner in message.notified_partner_ids])
if message.attachment_ids:
attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
# Read partners as SUPERUSER -> display the names like classic m2o even if no access
@@ -322,7 +322,7 @@ class mail_message(osv.Model):
else:
author = (0, message.email_from)
partner_ids = []
- for partner in message.partner_ids:
+ for partner in message.notified_partner_ids:
if partner.id in partner_tree:
partner_ids.append(partner_tree[partner.id])
attachment_ids = []
@@ -861,7 +861,7 @@ class mail_message(osv.Model):
# message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id:
return True
-
+
# all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
@@ -884,8 +884,7 @@ class mail_message(osv.Model):
# notify
if partners_to_notify:
- self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
- notification_obj._notify(cr, uid, newid, context=context)
+ notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context)
message.refresh()
# An error appear when a user receive a notification without notifying
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index 7d2e9483773..8f2fb3f1020 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -59,12 +59,12 @@
-
-
+ 9 %d + %d" % (cls, len(thread.message_ids), len(thread.message_follower_ids))
+ for result in cr.fetchall():
+ res[result[0]]['message_unread'] = True
+ res[result[0]]['message_unread_count'] += 1
+ for id in ids:
+ if res[id]['message_unread_count']:
+ title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
+ res[id]['message_summary'] = "9 %d %s" % (title, res[id].pop('message_unread_count'), _("New"))
return res
def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
@@ -607,15 +607,12 @@ class mail_thread(osv.AbstractModel):
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
+ nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
- new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
-
- # when posting an incoming email to a document: subscribe the author, if a partner, as follower
- if model and thread_id and msg.get('author_id'):
- model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context)
+ new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
@@ -760,7 +757,7 @@ class mail_thread(osv.AbstractModel):
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
msg_dict['message_id'] = message_id
- if 'Subject' in message:
+ if message.get('Subject'):
msg_dict['subject'] = decode(message.get('Subject'))
# Envelope fields not stored in mail.message but made available for message_new()
@@ -768,16 +765,15 @@ class mail_thread(osv.AbstractModel):
msg_dict['to'] = decode(message.get('to'))
msg_dict['cc'] = decode(message.get('cc'))
- if 'From' in message:
+ if message.get('From'):
author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
if author_ids:
msg_dict['author_id'] = author_ids[0]
- else:
- msg_dict['email_from'] = decode(message.get('from'))
+ msg_dict['email_from'] = decode(message.get('from'))
partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
- if 'Date' in message:
+ if message.get('Date'):
try:
date_hdr = decode(message.get('Date'))
parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
@@ -795,12 +791,12 @@ class mail_thread(osv.AbstractModel):
stored_date = datetime.datetime.now()
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- if 'In-Reply-To' in message:
+ if message.get('In-Reply-To'):
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
- if 'References' in message and 'parent_id' not in msg_dict:
+ if message.get('References') and 'parent_id' not in msg_dict:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
[x.strip() for x in decode(message['References']).split()])])
if parent_ids:
@@ -821,77 +817,151 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context)
- def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ def _message_add_suggested_recipient(self, cr, uid, result, obj, partner=None, email=None, reason='', context=None):
+ """ Called by message_get_suggested_recipients, to add a suggested
+ recipient in the result dictionary. The form is :
+ partner_id, partner_name or partner_name, reason """
+ if email and not partner:
+ partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context)[0]
+ if partner_info.get('partner_id'):
+ partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0]
+ if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip
+ return result
+ if partner and partner in obj.message_follower_ids: # recipient already in the followers -> skip
+ return result
+ if partner and partner in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
+ return result
+ if partner and partner.email: # complete profile: id, name
+ result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
+ elif partner: # incomplete profile: id, name
+ result[obj.id].append((partner.id, '%s' % (partner.name), reason))
+ else: # unknown partner, we are probably managing an email address
+ result[obj.id].append((False, email, reason))
+ return result
+
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ """ Returns suggested recipients for ids. Those are a list of
+ tuple (partner_id, partner_name, reason), to be managed by Chatter. """
+ result = dict.fromkeys(ids, list())
+ if self._all_columns.get('user_id'):
+ for obj in self.browse(cr, SUPERUSER_ID, ids, context=context): # SUPERUSER because of a read on res.users that would crash otherwise
+ if not obj.user_id or not obj.user_id.partner_id:
+ continue
+ self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
+ return result
+
+ def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
""" Convert a list of emails into a list partner_ids and a list
new_partner_ids. The return value is non conventional because
it is meant to be used by the mail widget.
:return dict: partner_ids and new_partner_ids
"""
- partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message')
-
- partner_ids = []
- new_partner_ids = []
+ partner_obj = self.pool.get('res.partner')
+ result = list()
for email in emails:
+ partner_info = {'full_name': email, 'partner_id': False}
m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
- name = m.group(2) or m.group(0)
- email = m.group(3)
- ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
+ if not m:
+ continue
+ email_address = m.group(3)
+ ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email_address)], context=context)
if ids:
- partner_ids.append(ids[0])
- partner_id = ids[0]
- else:
- partner_id = partner_obj.create(cr, uid, {
- 'name': name or email,
- 'email': email,
- }, context=context)
- new_partner_ids.append(partner_id)
+ partner_info['partner_id'] = ids[0]
+ result.append(partner_info)
# link mail with this from mail to the new partner id
- message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
- if message_ids:
- mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
- return {
- 'partner_ids': partner_ids,
- 'new_partner_ids': new_partner_ids,
- }
+ if link_mail and ids:
+ message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
+ '|',
+ ('email_from', '=', email),
+ ('email_from', 'ilike', '<%s>' % email),
+ ('author_id', '=', False)
+ ], context=context)
+ if message_ids:
+ mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': ids[0]}, context=context)
+ return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
- subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
+ subtype=None, parent_id=False, attachments=None, context=None,
+ content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new
- mail.message ID. Extra keyword arguments will be used as default
- column values for the new mail.message record.
- Auto link messages for same id and object
+ mail.message ID.
+
:param int thread_id: thread ID to post into, or list with one ID;
if False/0, mail.message model will also be set as False
:param str body: body of the message, usually raw HTML that will
be sanitized
- :param str subject: optional subject
- :param str type: mail_message.type
- :param int parent_id: optional ID of parent message in this thread
+ :param str type: see mail_message.type field
+ :param str content_subtype:: if plaintext: convert body into html
+ :param int parent_id: handle reply to a previous message by adding the
+ parent partners to the message in case of private discussion
:param tuple(str,str) attachments or list id: list of attachment tuples in the form
``(name,content)``, where content is NOT base64 encoded
- :return: ID of newly created mail.message
+
+ Extra keyword arguments will be used as default column values for the
+ new mail.message record. Special cases:
+ - attachment_ids: supposed not attached to any document; attach them
+ to the related document. Should only be set by Chatter.
+ :return int: ID of newly created mail.message
"""
if context is None:
context = {}
if attachments is None:
attachments = {}
-
- assert (not thread_id) or isinstance(thread_id, (int, long)) or \
- (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
- if isinstance(thread_id, (list, tuple)):
- thread_id = thread_id and thread_id[0]
mail_message = self.pool.get('mail.message')
+ ir_attachment = self.pool.get('ir.attachment')
+
+ assert (not thread_id) or \
+ isinstance(thread_id, (int, long)) or \
+ (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), \
+ "Invalid thread_id; should be 0, False, an ID or a list with one ID"
+ if isinstance(thread_id, (list, tuple)):
+ thread_id = thread_id[0]
# if we're processing a message directly coming from the gateway, the destination model was
- # set in the context.
+ # set in the context.
model = False
if thread_id:
model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
- attachment_ids = kwargs.pop('attachment_ids', [])
+ # 1: Handle content subtype: if plaintext, converto into HTML
+ if content_subtype == 'plaintext':
+ body = tools.plaintext2html(body)
+
+ # 2: Private message: add recipients (recipients and author of parent message)
+ # + legacy-code management (! we manage only 4 and 6 commands)
+ partner_ids = set()
+ kwargs_partner_ids = kwargs.pop('partner_ids', [])
+ for partner_id in kwargs_partner_ids:
+ if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
+ partner_ids.add(partner_id[1])
+ if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
+ partner_ids |= set(partner_id[2])
+ elif isinstance(partner_id, (int, long)):
+ partner_ids.add(partner_id)
+ else:
+ pass # we do not manage anything else
+ if parent_id and model == 'mail.thread':
+ parent_message = mail_message.browse(cr, uid, parent_id, context=context)
+ partner_ids |= set([partner.id for partner in parent_message.partner_ids])
+ if parent_message.author_id:
+ partner_ids.add(parent_message.author_id.id)
+
+ # 3. Attachments
+ # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
+ attachment_ids = kwargs.pop('attachment_ids', []) or [] # because we could receive None (some old code sends None)
+ if attachment_ids:
+ filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', 'mail.compose.message'),
+ ('res_id', '=', 0),
+ ('create_uid', '=', uid),
+ ('id', 'in', attachment_ids)], context=context)
+ if filtered_attachment_ids:
+ ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
+ attachment_ids = [(4, id) for id in attachment_ids]
+ # Handle attachments parameter, that is a dictionary of attachments
for name, content in attachments:
if isinstance(content, unicode):
content = content.encode('utf-8')
@@ -900,20 +970,25 @@ class mail_thread(osv.AbstractModel):
'datas': base64.b64encode(str(content)),
'datas_fname': name,
'description': name,
- 'res_model': context.get('thread_model') or self._name,
+ 'res_model': model,
'res_id': thread_id,
}
attachment_ids.append((0, 0, data_attach))
- # fetch subtype
+ # 4: mail.message.subtype
+ subtype_id = False
if subtype:
- s_data = subtype.split('.')
- if len(s_data) == 1:
- s_data = ('mail', s_data[0])
- ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s_data[0], s_data[1])
+ if '.' not in subtype:
+ subtype = 'mail.%s' % subtype
+ ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
subtype_id = ref and ref[1] or False
- else:
- subtype_id = False
+
+ # automatically subscribe recipients if asked to
+ if context.get('mail_post_autofollow') and thread_id and partner_ids:
+ partner_to_subscribe = partner_ids
+ if context.get('mail_post_autofollow_partner_ids'):
+ partner_to_subscribe = filter(lambda item: item in context.get('mail_post_autofollow_partner_ids'), partner_ids)
+ self.message_subscribe(cr, uid, [thread_id], list(partner_to_subscribe), context=context)
# _mail_flat_thread: automatically set free messages to the first posted message
if self._mail_flat_thread and not parent_id and thread_id:
@@ -941,86 +1016,34 @@ class mail_thread(osv.AbstractModel):
'parent_id': parent_id,
'attachment_ids': attachment_ids,
'subtype_id': subtype_id,
+ 'partner_ids': [(4, pid) for pid in partner_ids],
})
# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'):
values.pop(x, None)
- return mail_message.create(cr, uid, values, context=context)
+ # Create and auto subscribe the author
+ msg_id = mail_message.create(cr, uid, values, context=context)
+ message = mail_message.browse(cr, uid, msg_id, context=context)
+ if message.author_id and thread_id and type != 'notification':
+ self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
+ return msg_id
+
+ #------------------------------------------------------
+ # Compatibility methods: do not use
+ # TDE TODO: remove me in 8.0
+ #------------------------------------------------------
+
+ def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ return {'partner_ids': [], 'new_partner_ids': []}
def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
attachment_ids=None, content_subtype='plaintext',
context=None, **kwargs):
- """ Wrapper on message_post, used for user input :
- - mail gateway
- - quick reply in Chatter (refer to mail.js), not
- the mail.compose.message wizard
- The purpose is to perform some pre- and post-processing:
- - if body is plaintext: convert it into html
- - if parent_id: handle reply to a previous message by adding the
- parent partners to the message
- - type and subtype: comment and mail.mt_comment by default
- - attachment_ids: supposed not attached to any document; attach them
- to the related document. Should only be set by Chatter.
- """
- mail_message_obj = self.pool.get('mail.message')
- ir_attachment = self.pool.get('ir.attachment')
-
- # 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
- partner_ids = set([])
- if parent_id and self._name == 'mail.thread':
- parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
- partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
- if parent_message.author_id.id:
- partner_ids.add((4, parent_message.author_id.id))
-
- # 1.A.2: add specified recipients
- param_partner_ids = set()
- for item in kwargs.pop('partner_ids', []):
- if isinstance(item, (list)):
- param_partner_ids.add((item[0], item[1]))
- elif isinstance(item, (int, long)):
- param_partner_ids.add((4, item))
- else:
- param_partner_ids.add(item)
- partner_ids |= param_partner_ids
-
- # 1.A.3: add parameters recipients as follower
- # TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
- if param_partner_ids and self._name != 'mail.thread':
- self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
-
- # 1.B: handle body, message_type and message_subtype
- if content_subtype == 'plaintext':
- body = tools.plaintext2html(body)
- msg_type = kwargs.pop('type', 'comment')
- msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
-
- # 2. Pre-processing: attachments
- # HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
- if attachment_ids:
- # TDE FIXME (?): when posting a private message, we use mail.thread as a model
- # However, attaching doc to mail.thread is not possible, mail.thread does not have any table
- model = self._name
- if model == 'mail.thread':
- model = False
- filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
- ('res_model', '=', 'mail.compose.message'),
- ('res_id', '=', 0),
- ('create_uid', '=', uid),
- ('id', 'in', attachment_ids)], context=context)
- if filtered_attachment_ids:
- if thread_id and model:
- ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
- else:
- attachment_ids = []
- attachment_ids = [(4, id) for id in attachment_ids]
-
- # 3. Post message
- return self.message_post(cr, uid, thread_id=thread_id, body=body,
- type=msg_type, subtype=msg_subtype, parent_id=parent_id,
- attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
+ return self.message_post(cr, uid, thread_id, body=body, parent_id=parent_id,
+ attachment_ids=attachment_ids, content_subtype=content_subtype,
+ context=context, **kwargs)
#------------------------------------------------------
# Followers API
@@ -1142,12 +1165,25 @@ class mail_thread(osv.AbstractModel):
# add followers coming from res.users relational fields that are tracked
user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
- for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
+ user_id_partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]
+ for partner_id in user_id_partner_ids:
new_followers.setdefault(partner_id, None)
for pid, subtypes in new_followers.items():
subtypes = list(subtypes) if subtypes is not None else None
self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
+
+ # find first email message, set it as unread for auto_subscribe fields for them to have a notification
+ if user_id_partner_ids:
+ msg_ids = self.pool.get('mail.message').search(cr, uid, [
+ ('model', '=', self._name),
+ ('res_id', '=', record.id),
+ ('type', '=', 'email')], limit=1, context=context)
+ if not msg_ids and record.message_ids:
+ msg_ids = [record.message_ids[-1].id]
+ if msg_ids:
+ self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=user_id_partner_ids, context=context)
+
return True
#------------------------------------------------------
diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml
index 6769fb0d063..75c86e4645d 100644
--- a/addons/mail/mail_thread_view.xml
+++ b/addons/mail/mail_thread_view.xml
@@ -6,7 +6,7 @@
mail.wallmail.message{
- 'default_model': 'res.users',
+ 'default_model': 'res.users',
'default_res_id': uid,
}mail.wallmail.message{
- 'default_model': 'res.users',
- 'default_res_id': uid,
+ 'default_model': 'res.users',
+ 'default_res_id': uid,
'search_default_message_unread': True
}
+ 'show_compose_message': False
+ }""/>
No message found and no message sent yet.
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 77f680bcab2..8527e60aba0 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -18,9 +18,10 @@
# along with this program. If not, see .
#
##############################################################################
-
+from openerp.tools.translate import _
from openerp.osv import fields, osv
+
class res_partner_mail(osv.Model):
""" Update partner to add a field about notification preferences """
_name = "res.partner"
@@ -29,19 +30,28 @@ class res_partner_mail(osv.Model):
_columns = {
'notification_email_send': fields.selection([
- ('all', 'All feeds'),
- ('comment', 'Comments and Emails'),
- ('email', 'Emails only'),
- ('none', 'Never')
- ], 'Receive Feeds by Email', required=True,
- help="Choose in which case you want to receive an email when you "\
- "receive new feeds."),
+ ('none', 'Never'),
+ ('email', 'Incoming Emails only'),
+ ('comment', 'Incoming Emails and Discussions'),
+ ('all', 'All Messages (discussions, emails, followed system notifications)'),
+ ], 'Receive Messages by Email', required=True,
+ help="Policy to receive emails for new messages pushed to your personal Inbox:\n"
+ "- Never: no emails are sent\n"
+ "- Incoming Emails only: for messages received by the system via email\n"
+ "- Incoming Emails and Discussions: for incoming emails along with internal discussions\n"
+ "- All Messages: for every notification you receive in your Inbox"),
}
_defaults = {
'notification_email_send': lambda *args: 'comment'
}
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ recipients = super(res_partner_mail, self).message_get_suggested_recipients(cr, uid, ids, context=context)
+ for partner in self.browse(cr, uid, ids, context=context):
+ self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
+ return recipients
+
def message_post(self, cr, uid, thread_id, **kwargs):
""" Override related to res.partner. In case of email message, set it as
private:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index a4aa56ae971..7df5d27c9d0 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -7,6 +7,9 @@
res.partner
+
+
+
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 41b0a46c2b4..f7c91939b98 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -113,13 +113,6 @@ class res_users(osv.Model):
thread_id = thread_id[0]
return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
- def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
- """ Redirect the posting of message on res.users to the related partner.
- This is done because when giving the context of Chatter on the
- various mailboxes, we do not have access to the current partner_id. """
- partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
- return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
-
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
@@ -139,6 +132,27 @@ class res_users(osv.Model):
self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context)
return True
+ def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
+ return self.pool.get('res.partner').message_get_partner_info_from_emails(cr, uid, emails, link_mail=link_mail, context=context)
+
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ partner_ids = []
+ for id in ids:
+ partner_ids.append(self.browse(cr, SUPERUSER_ID, id).partner_id.id)
+ return self.pool.get('res.partner').message_get_suggested_recipients(cr, uid, partner_ids, context=context)
+
+ #------------------------------------------------------
+ # Compatibility methods: do not use
+ # TDE TODO: remove me in 8.0
+ #------------------------------------------------------
+
+ def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
+ """ Redirect the posting of message on res.users to the related partner.
+ This is done because when giving the context of Chatter on the
+ various mailboxes, we do not have access to the current partner_id. """
+ partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
+ return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
+
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context)
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index e4e112cdfaf..1e1142d592c 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -54,6 +54,9 @@
min-height: 42px;
border: solid 1px rgba(0,0,0,0.03);
}
+.openerp .oe_mail .oe_msg.oe_msg_nobody{
+ background: #F8F8F8;
+}
.openerp .oe_mail .oe_msg .oe_msg_left{
position: absolute;
left:0; top: 0; bottom: 0; width: 40px;
@@ -224,20 +227,30 @@
height: 24px;
width: 100%;
}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_sep_word{
+ margin-right: 8px;
+ margin-left: 8px;
+}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact{
height: 24px;
width: 100%;
padding: 2px 4px;
+ color: #AAA;
+ cursor: text;
+}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_record {
+ font-size: 13px;
+ font-style: bold;
+ text-align: center;
+}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_inbox {
border: 1px solid #CCC;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: white;
font-size: 14px;
- color: #AAA;
font-style: italic;
- word-spacing: 3px;
- cursor: text;
}
/* d) I.E. tweaks for Message action icons */
@@ -269,20 +282,20 @@
margin-top: 4px;
margin-bottom: 4px;
}
-.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{
+.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list {
display: block;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from{
+.openerp .oe_mail .oe_msg_composer .oe_recipients {
font-size: 12px;
margin-left: 20px;
margin-bottom: 2px;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from label{
+.openerp .oe_mail .oe_msg_composer .oe_recipients label{
vertical-align: middle;
display: block;
line-height: 14px;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from input{
+.openerp .oe_mail .oe_msg_composer .oe_recipients input{
vertical-align: middle;
}
.openerp .oe_mail .oe_attachment{
@@ -439,7 +452,8 @@
line-height: 12px;
vertical-align: middle;
}
-.openerp .oe_mail .oe_msg_footer button.oe_post{
+.openerp .oe_mail .oe_msg_footer button.oe_post,
+.openerp .oe_mail .oe_msg_footer button.oe_log{
position: relative;
z-index: 2;
}
@@ -458,18 +472,20 @@
.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{
display:inline;
}
-.openerp .oe_mail .oe_msg_footer button.oe_full{
- width:24px;
- overflow:hidden;
+.openerp .oe_mail .oe_msg_center button.oe_full{
+ width: 24px;
+ height: 22px;
+ overflow: hidden;
float: right;
- filter:none;
+ filter: none;
}
-.openerp .oe_mail .oe_msg_footer button.oe_full .oe_e{
+.openerp .oe_mail .oe_msg_center button.oe_full .oe_e{
position: relative;
- top: -4px;
- margin-left: -9px;
- vertical-align: middle;
- filter:none;
+ top: -9px;
+ margin-left: -5px;
+ vertical-align: top;
+ filter: none;
+ height: 14px;
}
.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{
background: transparent;
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index e5c8f40eb2e..f6e098b466d 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -21,6 +21,19 @@ openerp.mail = function (session) {
mail.ChatterUtils = {
+ /** parse text to find email: Tagada -> [Tagada, address@mail.fr] or False */
+ parse_email: function (text) {
+ var result = text.match(/(.*)<(.*@.*)>/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[2])];
+ }
+ result = text.match(/(.*@.*)/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[1])];
+ }
+ return [text, false];
+ },
+
/* Get an image in /web/binary/image?... */
get_image: function (session, model, field, id, resize) {
var r = resize ? encodeURIComponent(resize) : '';
@@ -255,11 +268,15 @@ openerp.mail = function (session) {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
}
if (this.author_id && this.author_id[1]) {
- var email = this.author_id[1].match(/(.*)<(.*@.*)>/);
- if (!email) {
- this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]);
- } else {
- this.author_id.push(_.str.escapeHTML(email[0]), _.str.trim(email[1]), email[2]);
+ var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]);
+ this.author_id.push(parsed_email[0], parsed_email[1]);
+ }
+ if (this.partner_ids && this.partner_ids.length > 3) {
+ this.extra_partners_nbr = this.partner_ids.length - 3;
+ this.extra_partners_str = ''
+ var extra_partners = this.partner_ids.slice(3);
+ for (var key in extra_partners) {
+ this.extra_partners_str += extra_partners[key][1];
}
}
},
@@ -371,14 +388,24 @@ openerp.mail = function (session) {
* @param {Object} [context] context passed to the
* mail.compose.message DataSetSearch. Please refer to this model
* for more details about fields and default values.
+ * @param {Object} recipients = [
+ {
+ 'email_address': [str],
+ 'partner_id': False/[int],
+ 'name': [str],
+ 'full_name': name,
+ },
+ { ... },
+ ]
*/
init: function (parent, datasets, options) {
this._super(parent, datasets, options);
this.show_compact_message = false;
this.show_delete_attachment = true;
- this.emails_from = [];
- this.partners_from = [];
+ this.is_log = false;
+ this.recipients = [];
+ this.recipient_ids = [];
},
start: function () {
@@ -479,15 +506,13 @@ openerp.mail = function (session) {
bind_events: function () {
var self = this;
-
- this.$('.oe_compact').on('click', _.bind( this.on_compose_expandable, this));
-
- // set the function called when attachments are added
- this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this) );
-
- this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) );
- this.$('.oe_post').on('click', _.bind( this.on_message_post, this) );
- this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') );
+ this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer);
+ this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this));
+ this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this));
+ this.$('.oe_post').on('click', self.on_message_post);
+ this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment'));
/* stack for don't close the compose form if the user click on a button */
this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
@@ -497,13 +522,12 @@ openerp.mail = function (session) {
this.$('textarea').autosize();
// auto close
- this.$('textarea').on('blur', _.bind( this.on_compose_expandable, this));
+ this.$('textarea').on('blur', self.on_toggle_quick_composer);
// event: delete child attachments off the oe_msg_attachment_list box
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
- this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from);
- this.$(".oe_partners_from").on('change', 'input', this.on_checked_partner_from);
+ this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient);
},
on_compose_fullmail: function (default_composition_mode) {
@@ -511,16 +535,26 @@ openerp.mail = function (session) {
if(!this.do_check_attachment_upload()) {
return false;
}
-
- // create list of new partners
- this.check_recipient_partners().done(function (partner_ids) {
+ var recipient_done = $.Deferred();
+ if (this.is_log) {
+ recipient_done.resolve([]);
+ }
+ else {
+ recipient_done = this.check_recipient_partners();
+ }
+ $.when(recipient_done).done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
'default_partner_ids': partner_ids,
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
};
+ if (self.is_log) {
+ _.extend(context, {'mail_compose_log': true});
+ }
if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id;
@@ -573,68 +607,122 @@ openerp.mail = function (session) {
check_recipient_partners: function () {
var self = this;
- var partners_from = [];
- var emails = [];
- _.each(this.emails_from, function (email_from) {
- if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) {
- emails.push(email_from[0][1]);
- }
- });
- var deferred_check = $.Deferred();
- if (emails.length == 0) {
- return deferred_check.resolve(partners_from);
+ var check_done = $.Deferred();
+ var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked });
+ var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id) });
+ var names_to_find = _.pluck(recipients_to_find, 'full_name');
+ var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address) });
+ var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address }), 'partner_id');
+ var names_to_remove = [];
+ var recipient_ids_to_remove = [];
+
+ // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
+ var find_done = $.Deferred();
+ if (names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find]);
}
- self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) {
- partners_from = _.clone(partners.partner_ids);
- var deferreds = [];
- _.each(partners.new_partner_ids, function (id) {
+ else {
+ find_done.resolve([]);
+ }
+
+ // for unknown names + incomplete partners -> open popup - cancel = remove from recipients
+ $.when(find_done).pipe(function (result) {
+ var emails_deferred = [];
+ var recipient_popups = result.concat(recipients_to_check);
+
+ _.each(recipient_popups, function (partner_info) {
var deferred = $.Deferred()
- deferreds.push(deferred);
- var pop = new session.web.form.FormOpenPopup(this);
+ emails_deferred.push(deferred);
+
+ var partner_name = partner_info.full_name;
+ var partner_id = partner_info.partner_id;
+ var parsed_email = mail.ChatterUtils.parse_email(partner_name);
+
+ var pop = new session.web.form.FormOpenPopup(this);
pop.show_element(
'res.partner',
- id,
- {
- 'force_email': true,
+ partner_id,
+ { 'force_email': true,
'ref': "compound_context",
- },
- {
+ 'default_name': parsed_email[0],
+ 'default_email': parsed_email[1],
+ }, {
title: _t("Please complete partner's informations"),
}
);
pop.on('closed', self, function () {
deferred.resolve();
});
- partners_from.push(id);
+ pop.view_form.on('on_button_cancel', self, function () {
+ names_to_remove.push(partner_name);
+ if (partner_id) {
+ recipient_ids_to_remove.push(partner_id);
+ }
+ });
});
- $.when.apply( $, deferreds ).then(function () {
- deferred_check.resolve(partners_from);
+
+ $.when.apply($, emails_deferred).then(function () {
+ var new_names_to_find = _.difference(names_to_find, names_to_remove);
+ find_done = $.Deferred();
+ if (new_names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find, true]);
+ }
+ else {
+ find_done.resolve([]);
+ }
+ $.when(find_done).pipe(function (result) {
+ var recipient_popups = result.concat(recipients_to_check);
+ _.each(recipient_popups, function (partner_info) {
+ if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
+ recipient_ids.push(partner_info.partner_id);
+ }
+ });
+ }).pipe(function () {
+ check_done.resolve(recipient_ids);
+ });
});
});
- return deferred_check;
+
+ return check_done;
},
on_message_post: function (event) {
var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
- // create list of new partners
- this.check_recipient_partners().done(function (partner_ids) {
- self.do_send_message_post(partner_ids);
- });
+ if (this.is_log) {
+ this.do_send_message_post([], this.is_log);
+ }
+ else {
+ this.check_recipient_partners().done(function (partner_ids) {
+ self.do_send_message_post(partner_ids, self.is_log);
+ });
+ }
}
},
- /*do post a message and fetch the message*/
- do_send_message_post: function (partner_ids) {
+ /* do post a message and fetch the message */
+ do_send_message_post: function (partner_ids, log) {
var self = this;
- this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
+ var values = {
'body': this.$('textarea').val(),
'subject': false,
'parent_id': this.context.default_parent_id,
'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
'partner_ids': partner_ids,
- 'context': this.parent_thread.context,
- }).done(function (message_id) {
+ 'context': _.extend(this.parent_thread.context, {
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ }),
+ 'type': 'comment',
+ 'content_subtype': 'plaintext',
+ };
+ if (log) {
+ values['subtype'] = false;
+ }
+ else {
+ values['subtype'] = 'mail.mt_comment';
+ }
+ this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) {
var thread = self.parent_thread;
var root = thread == self.options.root_thread;
if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
@@ -650,18 +738,55 @@ openerp.mail = function (session) {
});
},
- /* convert the compact mode into the compose message
- */
- on_compose_expandable: function (event) {
- this.get_emails_from();
- if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/) && !this.attachment_ids.length)) {
- this.show_composer = !this.show_composer || this.stay_open;
- this.reinit();
+ /* Quick composer: toggle minimal / expanded mode
+ * - toggle minimal (one-liner) / expanded (textarea, buttons) mode
+ * - when going into expanded mode:
+ * - call `message_get_suggested_recipients` to have a list of partners to add
+ * - compute email_from list (list of unknown email_from to propose to create partners)
+ */
+ on_toggle_quick_composer: function (event) {
+ var self = this;
+ var $input = $(event.target);
+ this.compute_emails_from();
+ var email_addresses = _.pluck(this.recipients, 'email_address');
+ var suggested_partners = $.Deferred();
+
+ // if clicked: call for suggested recipients
+ if (event.type == 'click') {
+ this.is_log = $input.hasClass('oe_compose_log');
+ suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id]]).done(function (additional_recipients) {
+ var thread_recipients = additional_recipients[self.context.default_res_id];
+ _.each(thread_recipients, function (recipient) {
+ var parsed_email = mail.ChatterUtils.parse_email(recipient[1]);
+ if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
+ self.recipients.push({
+ 'checked': true,
+ 'partner_id': recipient[0],
+ 'full_name': recipient[1],
+ 'name': parsed_email[0],
+ 'email_address': parsed_email[1],
+ 'reason': recipient[2],
+ })
+ }
+ });
+ });
}
- if (!this.stay_open && this.show_composer && (!event || event.type != 'blur')) {
- this.$('textarea:not(.oe_compact):first').focus();
+ else {
+ suggested_partners.resolve({});
}
- return true;
+
+ // when call for suggested partners finished: re-render the widget
+ $.when(suggested_partners).pipe(function (additional_recipients) {
+ if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) {
+ self.show_composer = !self.show_composer || self.stay_open;
+ self.reinit();
+ }
+ if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) {
+ self.$('textarea:not(.oe_compact):first').focus();
+ }
+ });
+
+ return suggested_partners;
},
do_hide_compact: function () {
@@ -678,7 +803,10 @@ openerp.mail = function (session) {
}
},
- get_emails_from: function () {
+ /** Compute the list of unknown email_from the the given thread
+ * TDE FIXME: seems odd to delegate to the composer
+ * TDE TODO: please de-obfuscate and comment your code */
+ compute_emails_from: function () {
var self = this;
var messages = [];
@@ -691,22 +819,28 @@ openerp.mail = function (session) {
// get all wall messages if is not a mail.Wall
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
}
-
+
_.each(messages, function (thread) {
if (thread.author_id && !thread.author_id[0] &&
- !_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) {
- self.emails_from.push([thread.author_id, true]);
+ !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];})) {
+ self.recipients.push({ 'full_name': thread.author_id[1],
+ 'name': thread.author_id[2],
+ 'email_address': thread.author_id[3],
+ 'partner_id': false,
+ 'checked': true,
+ 'reason': 'Incoming email author'
+ });
}
});
- return self.emails_from;
+ return self.recipients;
},
- on_checked_email_from: function (event) {
+ on_checked_recipient: function (event) {
var $input = $(event.target);
var email = $input.attr("data");
- _.each(this.emails_from, function (email_from) {
- if (email_from[0][4] == email) {
- email_from[1] = $input.is(":checked");
+ _.each(this.recipients, function (recipient) {
+ if (recipient.email_address == email) {
+ recipient.checked = $input.is(":checked");
}
});
},
@@ -1236,12 +1370,12 @@ openerp.mail = function (session) {
/**
*If compose_message doesn't exist, instantiate the compose message.
- * Call the on_compose_expandable method to allow the user to write his message.
+ * Call the on_toggle_quick_composer method to allow the user to write his message.
* (Is call when a user click on "Reply" button)
*/
on_compose_message: function (event) {
this.instantiate_compose_message();
- this.compose_message.on_compose_expandable(event);
+ this.compose_message.on_toggle_quick_composer(event);
return false;
},
@@ -1810,7 +1944,7 @@ openerp.mail = function (session) {
};
session.client.action_manager.do_action(action);
});
- this.$(".oe_write_onwall").click(function(){ self.root.thread.on_compose_message(); });
+ this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
}
});
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 41634fd2135..5dfc1946587 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -106,9 +106,12 @@ openerp_mail_followers = function(session, mail) {
on_remove_follower: function (event) {
var partner_id = $(event.target).data('id');
- var context = new session.web.CompoundContext(this.build_context(), {});
- return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
- .then(this.proxy('read_value'));
+ var name = $(event.target).parent().find("a").html();
+ if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
+ .then(this.proxy('read_value'));
+ }
},
read_value: function () {
@@ -248,12 +251,15 @@ openerp_mail_followers = function(session, mail) {
},
do_unfollow: function () {
- _(this.$('.oe_msg_subtype_check')).each(function (record) {
- $(record).attr('checked',false);
- });
- var context = new session.web.CompoundContext(this.build_context(), {});
- return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
- .then(this.proxy('read_value'));
+ if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
+ _(this.$('.oe_msg_subtype_check')).each(function (record) {
+ $(record).attr('checked',false);
+ });
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
+ .then(this.proxy('read_value'));
+ }
+ return false;
},
do_update_subscription: function (event) {
@@ -267,7 +273,9 @@ openerp_mail_followers = function(session, mail) {
});
if (!checklist.length) {
- this.do_unfollow();
+ if (!this.do_unfollow()) {
+ $(event.target).attr("checked", "checked");
+ }
} else {
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context])
diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml
index 4693adeb1ee..b26ef06f5e4 100644
--- a/addons/mail/static/src/xml/mail.xml
+++ b/addons/mail/static/src/xml/mail.xml
@@ -26,24 +26,35 @@
+ ò
-
-
- Write to the followers of this document...
- Share with my followers...
-
@@ -245,6 +259,22 @@
+
+ logged a note
+
+
+ to
+
+
+
+
+
+
+ ,
+
+
+ and more
+ ••
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index 4302dab8669..b327bd33d98 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -119,18 +119,18 @@ class test_mail(TestMailBase):
# Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
group_messages = frog_group.message_ids
- self.assertTrue(len(group_messages) == 2, 'New group should only have the original message + creation log')
+ self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs ', subject='news', extra='')
self.mail_thread.message_process(cr, uid, None, mail_frog_news)
frog_group.refresh()
- self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
+ self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
# Even with a wrong destination, a reply should end up in the correct thread
mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
self.mail_thread.message_process(cr, uid, None, mail_reply)
frog_group.refresh()
- self.assertTrue(len(frog_group.message_ids) == 4, 'Group should contain 4 messages now')
+ self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
# No model passed and no matching alias must raise
mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
@@ -154,7 +154,7 @@ class test_mail(TestMailBase):
new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
# Test: author_id set, not email_from
self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
- self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found')
+ self.assertEqual(new_mail.email_from, user_raoul.email, 'message process wrong email_from')
# Do: post a new message, with a unknown partner
test_msg_id = ''
@@ -391,7 +391,8 @@ class test_mail(TestMailBase):
# 1. Post a new email comment on Pigs
self._init_mock_build_email()
msg2_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
- partner_ids=[(6, 0, [p_d_id])], parent_id=msg1_id, attachments=_attachments)
+ partner_ids=[p_d_id], parent_id=msg1_id, attachments=_attachments,
+ context={'mail_post_autofollow': True})
message2 = self.mail_message.browse(cr, uid, msg2_id)
sent_emails = self._build_email_kwargs_list
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg2_id)]), 'mail.mail notifications should have been auto-deleted!')
@@ -482,7 +483,7 @@ class test_mail(TestMailBase):
self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
# 2. Post the comment, get created message
- mail_compose.send_mail(cr, uid, [compose_id])
+ mail_compose.send_mail(cr, uid, [compose_id], {'mail_post_autofollow': True})
group_pigs.refresh()
message = group_pigs.message_ids[0]
# Test: mail.message: subject, body inside pre
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py
index 07a87a16910..f422219000c 100644
--- a/addons/mail/tests/test_mail_message.py
+++ b/addons/mail/tests/test_mail_message.py
@@ -311,8 +311,8 @@ class test_mail_access_rights(TestMailBase):
user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id
# Prepare groups: Pigs (employee), Jobs (public)
- pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[(4, self.partner_admin_id)])
- jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[(4, self.partner_admin_id)])
+ pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[self.partner_admin_id])
+ jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[self.partner_admin_id])
# ----------------------------------------
# CASE1: Bert, without groups
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
index 72678d468ff..a7cdc237a60 100644
--- a/addons/mail/wizard/invite.py
+++ b/addons/mail/wizard/invite.py
@@ -66,7 +66,7 @@ class invite_wizard(osv.osv_memory):
signature = user_id and user_id["signature"] or ''
if signature:
wizard.message = tools.append_content_to_html(wizard.message, signature, plaintext=True, container_tag='div')
- # send mail to new followers
+ # FIXME 8.0: use notification_email_send, send a wall message and let mail handle email notification + message box
for follower_id in new_follower_ids:
mail_mail = self.pool.get('mail.mail')
# the invite wizard should create a private message not related to any object -> no model, no res_id
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index ec0c2a8de06..5776a8e46c5 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -191,6 +191,7 @@ class mail_compose_message(osv.TransientModel):
if context is None:
context = {}
active_ids = context.get('active_ids')
+ is_log = context.get('mail_compose_log', False)
for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail'
@@ -204,26 +205,22 @@ class mail_compose_message(osv.TransientModel):
'subject': wizard.subject,
'body': wizard.body,
'parent_id': wizard.parent_id and wizard.parent_id.id,
- 'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
+ 'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
}
# mass mailing: render and override default values
if mass_mail_mode and wizard.model:
email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
new_partner_ids = email_dict.pop('partner_ids', [])
- post_values['partner_ids'] += [(4, partner_id) for partner_id in new_partner_ids]
+ post_values['partner_ids'] += new_partner_ids
new_attachments = email_dict.pop('attachments', [])
post_values['attachments'] += new_attachments
post_values.update(email_dict)
- # automatically subscribe recipients if asked to
- if context.get('mail_post_autofollow') and wizard.model and post_values.get('partner_ids'):
- active_model_pool.message_subscribe(cr, uid, [res_id], [item[1] for item in post_values.get('partner_ids')], context=context)
# post the message
- active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values)
-
- # post process: update attachments, because id is not necessarily known when adding attachments in Chatter
- # self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], {
- # 'res_id': wizard.id, 'res_model': wizard.model or False}, context=context)
+ subtype = 'mail.mt_comment'
+ if is_log:
+ subtype = False
+ active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
return {'type': 'ir.actions.act_window_close'}
@@ -258,7 +255,7 @@ class mail_compose_message(osv.TransientModel):
result = eval(exp, {
'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object': self.pool.get(model).browse(cr, uid, res_id, context=context),
- 'context': dict(context), # copy context to prevent side-effects of eval
+ 'context': dict(context), # copy context to prevent side-effects of eval
})
return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template)
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index fca5ec65ca0..00ba33a6d6f 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -13,8 +13,9 @@
-
-
+
+
Followers of selected items and
diff --git a/addons/marketing_campaign/marketing_campaign_view.xml b/addons/marketing_campaign/marketing_campaign_view.xml
index 2b479be709c..2d7501a8333 100644
--- a/addons/marketing_campaign/marketing_campaign_view.xml
+++ b/addons/marketing_campaign/marketing_campaign_view.xml
@@ -59,7 +59,7 @@
-
+
@@ -190,7 +190,7 @@
-
+
@@ -387,7 +387,7 @@
-
+
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index bde5abee2b5..f3da234463c 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -625,8 +625,8 @@
-
-
+
+
diff --git a/addons/mrp_operations/mrp_operations_view.xml b/addons/mrp_operations/mrp_operations_view.xml
index dbe5ed83d0d..207c5d975d7 100644
--- a/addons/mrp_operations/mrp_operations_view.xml
+++ b/addons/mrp_operations/mrp_operations_view.xml
@@ -10,7 +10,7 @@
-
+
diff --git a/addons/point_of_sale/account_bank_statement.py b/addons/point_of_sale/account_bank_statement.py
index 4f88f8e3650..31415bb847c 100644
--- a/addons/point_of_sale/account_bank_statement.py
+++ b/addons/point_of_sale/account_bank_statement.py
@@ -27,7 +27,7 @@ class account_journal(osv.osv):
_columns = {
'journal_user': fields.boolean('PoS Payment Method', help="Check this box if this journal define a payment method that can be used in point of sales."),
- 'amount_authorized_diff' : fields.float('Amount Authorized Difference'),
+ 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theorical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."),
'self_checkout_payment_method' : fields.boolean('Self Checkout Payment Method'),
}
_defaults = {
diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py
index ea7460dae21..af5d7aa663c 100644
--- a/addons/point_of_sale/point_of_sale.py
+++ b/addons/point_of_sale/point_of_sale.py
@@ -704,6 +704,7 @@ class pos_order(osv.osv):
@return: True
"""
stock_picking_obj = self.pool.get('stock.picking')
+ wf_service = netsvc.LocalService("workflow")
for order in self.browse(cr, uid, ids, context=context):
stock_picking_obj.signal_button_cancel(cr, uid, [order.picking_id.id])
if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
@@ -974,8 +975,12 @@ class pos_order(osv.osv):
else:
grouped_data[key].append(values)
+ #because of the weird way the pos order is written, we need to make sure there is at least one line,
+ #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
+ #are set inside the for loop)
+ #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack
+ assert order.lines, _('The POS order must have lines when calling this method')
# Create an move for each order line
-
for line in order.lines:
tax_amount = 0
taxes = [t for t in line.product_id.taxes_id]
@@ -1050,7 +1055,7 @@ class pos_order(osv.osv):
'name': _('Tax') + ' ' + tax.name,
'quantity': line.qty,
'product_id': line.product_id.id,
- 'account_id': key[account_pos],
+ 'account_id': key[account_pos] or income_account,
'credit': ((tax_amount>0) and tax_amount) or 0.0,
'debit': ((tax_amount<0) and -tax_amount) or 0.0,
'tax_code_id': key[tax_code_pos],
@@ -1118,9 +1123,9 @@ class pos_order_line(osv.osv):
account_tax_obj = self.pool.get('account.tax')
cur_obj = self.pool.get('res.currency')
for line in self.browse(cr, uid, ids, context=context):
- taxes = line.product_id.taxes_id
+ taxes_ids = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ]
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
- taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
+ taxes = account_tax_obj.compute_all(cr, uid, taxes_ids, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
cur = line.order_id.pricelist_id.currency_id
res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js
index 2be58d6ce97..a79b111b5c5 100644
--- a/addons/point_of_sale/static/src/js/db.js
+++ b/addons/point_of_sale/static/src/js/db.js
@@ -178,7 +178,7 @@ function openerp_pos_db(instance, module){
var ancestors = this.get_category_ancestors_ids(categ_id) || [];
- for(var j = 0; j < ancestors.length; j++){
+ for(var j = 0, jlen = ancestors.length; j < jlen; j++){
var ancestor = ancestors[j];
if(! stored_categories[ancestor]){
stored_categories[ancestor] = [];
diff --git a/addons/point_of_sale/test/01_order_to_payment.yml b/addons/point_of_sale/test/01_order_to_payment.yml
index 9534e79095f..bf201affc4a 100644
--- a/addons/point_of_sale/test/01_order_to_payment.yml
+++ b/addons/point_of_sale/test/01_order_to_payment.yml
@@ -27,10 +27,23 @@
account_collected_id: account.iva
price_include: 0
-
- I assign this 5 percent tax on the PCSC349 product as a sale tax
+ I will create a second VAT tax of 5% but this time for a child company, to
+ ensure that only product taxes of the current session's company are considered
+ (this tax should be ignore when computing order's taxes in following tests)
+-
+ !record {model: account.tax, id: account_tax_05_incl_chicago}:
+ name: VAT 05 perc Excl (US)
+ type: percent
+ amount: 0.05
+ account_paid_id: account.iva
+ account_collected_id: account.iva
+ price_include: 0
+ company_id: stock.res_company_1
+-
+ I assign those 5 percent taxes on the PCSC349 product as a sale taxes
-
!record {model: product.product, id: product.product_product_4}:
- taxes_id: [account_tax_05_incl]
+ taxes_id: [account_tax_05_incl, account_tax_05_incl_chicago]
-
I create a new session
-
diff --git a/addons/portal/portal_view.xml b/addons/portal/portal_view.xml
index 203dcde90c7..d56e277c3fb 100644
--- a/addons/portal/portal_view.xml
+++ b/addons/portal/portal_view.xml
@@ -9,12 +9,13 @@
sequence="15"/>
+
Inboxmail.wallmail.message{
- 'default_model': 'res.users',
+ 'default_model': 'res.users',
'default_res_id': uid,
}
+ To-do
+ mail.wall
+ mail.message
+ {
+ 'default_model': 'res.users',
+ 'default_res_id': uid,
+ 'search_default_message_unread': True
+ }
+
+
+
+ No todo.
+
+ When you process messages in your inbox, you can mark some
+ as todo. From this menu, you can process all your todo.
+