diff --git a/addons/account_anglo_saxon/invoice.py b/addons/account_anglo_saxon/invoice.py
index 8b31a48b92b..81394bc926e 100644
--- a/addons/account_anglo_saxon/invoice.py
+++ b/addons/account_anglo_saxon/invoice.py
@@ -21,21 +21,25 @@
#
##############################################################################
-from openerp.osv import osv
+from openerp.osv import osv, fields
class account_invoice_line(osv.osv):
_inherit = "account.invoice.line"
+ _columns = {
+ 'move_id': fields.many2one('stock.move', string="Move line", help="If the invoice was generated from a stock.picking, reference to the related move line."),
+ }
+
def move_line_get(self, cr, uid, invoice_id, context=None):
res = super(account_invoice_line,self).move_line_get(cr, uid, invoice_id, context=context)
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
company_currency = inv.company_id.currency_id.id
- def get_price(cr, uid, inv, company_currency,i_line):
+ def get_price(cr, uid, inv, company_currency, i_line, price_unit):
cur_obj = self.pool.get('res.currency')
if inv.currency_id.id != company_currency:
- price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, i_line.product_id.standard_price * i_line.quantity, context={'date': inv.date_invoice})
+ price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice})
else:
- price = i_line.product_id.standard_price * i_line.quantity
+ price = price_unit * i_line.quantity
return price
if inv.type in ('out_invoice','out_refund'):
@@ -60,12 +64,13 @@ class account_invoice_line(osv.osv):
if not cacc:
cacc = i_line.product_id.categ_id.property_account_expense_categ and i_line.product_id.categ_id.property_account_expense_categ.id
if dacc and cacc:
+ price_unit = i_line.move_id and i_line.move_id.price_unit or i_line.product_id.standard_price
res.append({
'type':'src',
'name': i_line.name[:64],
- 'price_unit':i_line.product_id.standard_price,
+ 'price_unit':price_unit,
'quantity':i_line.quantity,
- 'price':get_price(cr, uid, inv, company_currency, i_line),
+ 'price':get_price(cr, uid, inv, company_currency, i_line, price_unit),
'account_id':dacc,
'product_id':i_line.product_id.id,
'uos_id':i_line.uos_id.id,
@@ -76,9 +81,9 @@ class account_invoice_line(osv.osv):
res.append({
'type':'src',
'name': i_line.name[:64],
- 'price_unit':i_line.product_id.standard_price,
+ 'price_unit':price_unit,
'quantity':i_line.quantity,
- 'price': -1 * get_price(cr, uid, inv, company_currency, i_line),
+ 'price': -1 * get_price(cr, uid, inv, company_currency, i_line, price_unit),
'account_id':cacc,
'product_id':i_line.product_id.id,
'uos_id':i_line.uos_id.id,
diff --git a/addons/account_anglo_saxon/stock.py b/addons/account_anglo_saxon/stock.py
index 1cf3ecf21ef..278deb9a2a8 100644
--- a/addons/account_anglo_saxon/stock.py
+++ b/addons/account_anglo_saxon/stock.py
@@ -28,6 +28,15 @@ class stock_picking(osv.osv):
_inherit = "stock.picking"
_description = "Picking List"
+ def _prepare_invoice_line(self, cr, uid, group, picking, move_line, invoice_id,
+ invoice_vals, context=None):
+ """Overwrite to add move_id reference"""
+ res = super(stock_picking, self)._prepare_invoice_line(cr, uid, group, picking, move_line, invoice_id, invoice_vals, context=context)
+ res.update({
+ 'move_id': move_line.id,
+ })
+ return res
+
def action_invoice_create(self, cr, uid, ids, journal_id=False,
group=False, type='out_invoice', context=None):
'''Return ids of created invoices for the pickings'''
diff --git a/addons/auth_signup/auth_signup_data.xml b/addons/auth_signup/auth_signup_data.xml
index 043733873b1..f153f03237d 100644
--- a/addons/auth_signup/auth_signup_data.xml
+++ b/addons/auth_signup/auth_signup_data.xml
@@ -11,8 +11,8 @@
-
-
+
+ _usertemplate
diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py
index 66f7913c966..a9469e4de0d 100644
--- a/addons/auth_signup/res_users.py
+++ b/addons/auth_signup/res_users.py
@@ -234,8 +234,6 @@ class res_users(osv.Model):
# create a copy of the template user (attached to a specific partner_id if given)
values['active'] = True
- if 'alias_name' not in values: # allow behavior change via inheritance (like using the name)
- values['alias_name'] = False
context = dict(context or {}, no_reset_password=True)
return self.copy(cr, uid, template_user_id, values, context=context)
diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml
index 99b3296c3c1..bc066db0a7a 100644
--- a/addons/crm/crm_case_section_view.xml
+++ b/addons/crm/crm_case_section_view.xml
@@ -13,7 +13,6 @@
{
'search_default_section_id': [active_id],
- 'search_default_open': 1,
'default_section_id': active_id,
'default_type': 'lead',
'stage_type': 'lead',
@@ -43,7 +42,6 @@
{
'search_default_section_id': [active_id],
- 'search_default_assigned_to_me': 1,
'default_section_id': active_id,
'stage_type': 'opportunity',
'default_type': 'opportunity',
@@ -64,6 +62,26 @@
+
+ Leads Analysis
+ crm.lead.report
+ form
+ {"search_default_month":1}
+ tree,graph
+ [('type','=', 'lead'),('section_id', '=', active_id)]
+ Leads Analysis allows you to check different CRM related information like the treatment delays or number of leads per state. You can sort out your leads analysis by different groups to get accurate grained analysis.
+
+
+
+ Opportunities Analysis
+ crm.lead.report
+ form
+ {"search_default_month":1}
+ tree,graph
+ [('type','=', 'opportunity'), ('section_id', '=', active_id)]
+ Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the teams of the sales pipeline.
+
+
@@ -99,13 +117,25 @@
+
+
+
+
+
+
diff --git a/addons/crm/crm_demo.xml b/addons/crm/crm_demo.xml
index 2858250e86a..82bdcd8fcd9 100644
--- a/addons/crm/crm_demo.xml
+++ b/addons/crm/crm_demo.xml
@@ -6,14 +6,20 @@
+
+
+
+
Indirect Sales
IM
+
Marketing
SPD
+
diff --git a/addons/crm/report/crm_lead_report_view.xml b/addons/crm/report/crm_lead_report_view.xml
index 95cf1d4b7b0..c7220fe66f2 100644
--- a/addons/crm/report/crm_lead_report_view.xml
+++ b/addons/crm/report/crm_lead_report_view.xml
@@ -66,9 +66,6 @@
crm.lead.report
-
-
-
-
@@ -168,9 +165,9 @@
Leads Analysis
crm.lead.report
form
- {'search_default_year': 1,'search_default_lead': 1, "search_default_user":1, "search_default_this_month":1, 'group_by_no_leaf':1, 'group_by':[]}
+ {'search_default_year': 1, "search_default_user":1, "search_default_month":1, 'group_by_no_leaf':1, 'group_by':[]}
tree,graph
- []
+ [('type','=', 'lead')]
Leads Analysis allows you to check different CRM related information like the treatment delays or number of leads per state. You can sort out your leads analysis by different groups to get accurate grained analysis.
@@ -190,8 +187,9 @@
Opportunities Analysis
crm.lead.report
form
- {"search_default_year":1,"search_default_opportunity":1, "search_default_user":1,"search_default_this_month":1,'group_by_no_leaf':1,'group_by':[]}
+ {"search_default_year":1, "search_default_user":1,"search_default_month":1,'group_by_no_leaf':1,'group_by':[]}
tree,graph
+ [('type','=', 'opportunity')]
Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the teams of the sales pipeline.
diff --git a/addons/crm/security/crm_security.xml b/addons/crm/security/crm_security.xml
index ef7eb8b24db..efc335391f8 100644
--- a/addons/crm/security/crm_security.xml
+++ b/addons/crm/security/crm_security.xml
@@ -91,5 +91,19 @@
+
+ Personal Leads Analysis
+
+ ['|',('user_id','=',user.id),('user_id','=',False)]
+
+
+
+
+ All Leads Analysis
+
+ [(1,'=',1)]
+
+
+
diff --git a/addons/crm/static/src/css/Makefile b/addons/crm/static/src/css/Makefile
new file mode 100644
index 00000000000..a2ced24a13d
--- /dev/null
+++ b/addons/crm/static/src/css/Makefile
@@ -0,0 +1,2 @@
+crm.css: crm.sass
+ sass --trace -t expanded crm.sass:crm.css
diff --git a/addons/crm/static/src/css/crm.css b/addons/crm/static/src/css/crm.css
index cce8248bc8b..8999f758e41 100644
--- a/addons/crm/static/src/css/crm.css
+++ b/addons/crm/static/src/css/crm.css
@@ -1,67 +1,62 @@
+@charset "utf-8";
.openerp .oe_kanban_view .oe_kanban_crm_salesteams {
- /* Customize width and height of kanban according bootstrap3 */
- width: 357px;
- min-height: 254px !important;
- /* End of customize */
+ /* Customize width and height of kanban according bootstrap3 */
+ width: 357px;
+ min-height: 254px !important;
+ /* End of customize */
+ cursor: default;
}
-.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_avatars {
- text-align: right;
- margin: -5px 0 -10px 0;
-}
-
-.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_avatars img {
- width: 30px;
- height: 30px;
- padding-left: 0px;
- margin-top: 3px;
- -moz-border-radius: 2px;
- -webkit-border-radius: 2px;
- border-radius: 2px;
- -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
-}
-
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list {
- position: relative;
- margin: 10px 0 10px 9px; /* Improved margin to set alignment of list items according bootstrap3 */
+ position: relative;
+ /* Improved margin to set alignment of list items according bootstrap3 */
+ margin: 10px 0 10px 9px;
+ min-height: 10px;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list div {
- width: 160px;
- height: 22px;
- margin: 0 !important;
- position: relative;
- display: inline-block;
-}
-.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list a:hover {
- text-decoration: underline !important;
+ width: 160px;
+ height: 22px;
+ margin: 0 !important;
+ position: relative;
+ display: inline-block;
+ float: left;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list div a:nth-child(2n) {
- position: absolute;
- left: 90px;
- top: 0;
+ position: absolute;
+ left: 90px;
+ top: 0;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list div:nth-child(2n) a:nth-child(2n) {
- left: 110px;
+ left: 110px;
+}
+.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_items_list a:hover {
+ text-decoration: underline !important;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_center {
- text-align: center;
- margin: 3px 0;
+ text-align: center;
+ margin: 3px 0;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_center .oe_sum {
- margin: 0;
- font-size: 40px;
+ margin: 0;
+ font-size: 40px;
}
.openerp .oe_kanban_view .oe_kanban_crm_salesteams .oe_center .oe_subsum {
- font-size: 10px;
+ font-size: 10px;
}
-
-.openerp .oe_kanban_view .oe_justgage {
- color: black;
- display: inline-block;
+.openerp .oe_kanban_view .oe_salesteams_help {
+ display: inline-block;
+}
+.openerp .oe_kanban_view .oe_kanban_salesteams_avatars {
+ margin-top: 20px;
+}
+.openerp .oe_kanban_view .oe_kanban_salesteams_avatars img {
+ width: 30px;
+ height: 30px;
+ padding-left: 0px;
+ margin-top: 3px;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
-
-.openerp .oe_kanban_view .oe_sparkline_bar {
- height: 20px;
- width: 36px;
-}
\ No newline at end of file
diff --git a/addons/crm/static/src/css/crm.sass b/addons/crm/static/src/css/crm.sass
new file mode 100644
index 00000000000..5c70981e52f
--- /dev/null
+++ b/addons/crm/static/src/css/crm.sass
@@ -0,0 +1,55 @@
+@charset "utf-8"
+
+.openerp
+ .oe_kanban_view
+ .oe_kanban_crm_salesteams
+ /* Customize width and height of kanban according bootstrap3 */
+ width: 357px
+ min-height: 254px !important
+ /* End of customize */
+ cursor: default
+ .oe_items_list
+ position: relative
+ /* Improved margin to set alignment of list items according bootstrap3 */
+ margin: 10px 0 10px 9px
+ min-height: 10px
+ div
+ width: 160px
+ height: 22px
+ margin: 0 !important
+ position: relative
+ display: inline-block
+ float: left
+ a:nth-child(2n)
+ position: absolute
+ left: 90px
+ top: 0
+ div:nth-child(2n)
+ a:nth-child(2n)
+ left: 110px
+ a:hover
+ text-decoration: underline !important
+ .oe_center
+ text-align: center
+ margin: 3px 0
+ .oe_sum
+ margin: 0
+ font-size: 40px
+ .oe_subsum
+ font-size: 10px
+ .oe_salesteams_help
+ display: inline-block
+ .oe_kanban_salesteams_avatars
+ margin-top: 20px
+ .oe_kanban_salesteams_avatars
+ img
+ width: 30px
+ height: 30px
+ padding-left: 0px
+ margin-top: 3px
+ -moz-border-radius: 2px
+ -webkit-border-radius: 2px
+ border-radius: 2px
+ -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
+ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
+ -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
diff --git a/addons/crm/static/src/js/crm_case_section.js b/addons/crm/static/src/js/crm_case_section.js
index deb21d38de2..02f42288880 100644
--- a/addons/crm/static/src/js/crm_case_section.js
+++ b/addons/crm/static/src/js/crm_case_section.js
@@ -1,4 +1,40 @@
openerp.crm = function(openerp) {
+ openerp.web_kanban.KanbanView.include({
+ crm_display_members_names: function() {
+ /*
+ * Set avatar title for members.
+ * In kanban views, many2many fields only return a list of ids.
+ * We can implement return value of m2m fields like [(1,"Adminstration"),...].
+ */
+ var self = this;
+ var members_ids = [];
+
+ // Collect members ids
+ self.$el.find('img[data-member_id]').each(function() {
+ members_ids.push($(this).data('member_id'));
+ });
+
+ // Find their matching names
+ var dataset = new openerp.web.DataSetSearch(self, 'res.users', self.session.context, [['id', 'in', _.uniq(members_ids)]]);
+ dataset.read_slice(['id', 'name']).done(function(result) {
+ _.each(result, function(v, k) {
+ // Set the proper value in the DOM
+ self.$el.find('img[data-member_id=' + v.id + ']').attr('title', v.name).tipsy({
+ offset: 10
+ });
+ });
+ });
+ },
+ on_groups_started: function() {
+ var self = this;
+ self._super.apply(self, arguments);
+
+ if (self.dataset.model === 'crm.case.section') {
+ self.crm_display_members_names();
+ }
+ },
+ });
+
openerp.web_kanban.KanbanRecord.include({
on_card_clicked: function() {
if (this.view.dataset.model === 'crm.case.section') {
@@ -8,5 +44,4 @@ openerp.crm = function(openerp) {
}
},
});
-
};
diff --git a/addons/im_livechat/im_livechat.py b/addons/im_livechat/im_livechat.py
index b21a31d8362..e322467e64e 100644
--- a/addons/im_livechat/im_livechat.py
+++ b/addons/im_livechat/im_livechat.py
@@ -211,5 +211,5 @@ class im_session(osv.osv):
_inherit = 'im.session'
_columns = {
- 'channel_id': fields.many2one("im.user", "Channel"),
+ 'channel_id': fields.many2one("im_livechat.channel", "Channel"),
}
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index de8e4c68fda..9dfec55b075 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -817,12 +817,11 @@ class mail_message(osv.Model):
return email_reply_to
def _get_message_id(self, cr, uid, values, context=None):
- message_id = None
- if not values.get('message_id') and values.get('reply_to'):
+ if values.get('reply_to'):
message_id = tools.generate_tracking_message_id('reply_to')
- elif not values.get('message_id') and values.get('res_id') and values.get('model'):
+ elif values.get('res_id') and values.get('model'):
message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
- elif not values.get('message_id'):
+ else:
message_id = tools.generate_tracking_message_id('private')
return message_id
@@ -833,7 +832,7 @@ class mail_message(osv.Model):
if 'email_from' not in values: # needed to compute reply_to
values['email_from'] = self._get_default_from(cr, uid, context=context)
- if not values.get('message_id'):
+ if 'message_id' not in values:
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index b3bf1f922b5..ce3fc07a8ea 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -724,8 +724,8 @@ class mail_thread(osv.AbstractModel):
# Private message: should not contain any thread_id
if not model and thread_id:
if assert_model:
- assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).'
- _warn('posting a message without model should be with a null res_id (private message), resetting thread_id')
+ assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id
+ _warn('posting a message without model should be with a null res_id (private message), received %s, resetting thread_id' % thread_id)
thread_id = 0
# Private message: should have a parent_id (only answers)
if not model and not message_dict.get('parent_id'):
@@ -822,6 +822,7 @@ class mail_thread(osv.AbstractModel):
:return: list of [model, thread_id, custom_values, user_id, alias]
"""
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
+ mail_msg_obj = self.pool['mail.message']
fallback_model = model
# Get email.message.Message variables for future processing
@@ -830,31 +831,54 @@ class mail_thread(osv.AbstractModel):
email_to = decode_header(message, 'To')
references = decode_header(message, 'References')
in_reply_to = decode_header(message, 'In-Reply-To')
-
- # 1. Verify if this is a reply to an existing thread
thread_references = references or in_reply_to
+
+ # 1. message is a reply to an existing message (exact match of message_id)
+ msg_references = thread_references.split()
+ mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
+ if mail_message_ids:
+ original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
+ model, thread_id = original_msg.model, original_msg.res_id
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, uid)
+ route = self.message_route_verify(
+ cr, uid, message, message_dict,
+ (model, thread_id, custom_values, uid, None),
+ update_author=True, assert_model=True, create_fallback=True, context=context)
+ return route and [route] or []
+
+ # 2. message is a reply to an existign thread (6.1 compatibility)
ref_match = thread_references and tools.reference_re.search(thread_references)
if ref_match:
thread_id = int(ref_match.group(1))
model = ref_match.group(2) or fallback_model
if thread_id and model in self.pool:
model_obj = self.pool[model]
- if model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
- _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, uid)
- route = self.message_route_verify(cr, uid, message, message_dict,
- (model, thread_id, custom_values, uid, None),
- update_author=True, assert_model=True, create_fallback=True, context=context)
+ compat_mail_msg_ids = mail_msg_obj.search(
+ cr, uid, [
+ ('message_id', '=', False),
+ ('model', '=', model),
+ ('res_id', '=', thread_id),
+ ], context=context)
+ if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, uid)
+ route = self.message_route_verify(
+ cr, uid, message, message_dict,
+ (model, thread_id, custom_values, uid, None),
+ update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
# 2. Reply to a private message
if in_reply_to:
- mail_message_ids = self.pool.get('mail.message').search(cr, uid, [
+ mail_message_ids = mail_msg_obj.search(cr, uid, [
('message_id', '=', in_reply_to),
'!', ('message_id', 'ilike', 'reply_to')
], limit=1, context=context)
if mail_message_ids:
- mail_message = self.pool.get('mail.message').browse(cr, uid, mail_message_ids[0], context=context)
+ mail_message = mail_msg_obj.browse(cr, uid, mail_message_ids[0], context=context)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, mail_message.id, custom_values, uid)
route = self.message_route_verify(cr, uid, message, message_dict,
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index ac1d406c29a..e335929ec2f 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -81,6 +81,12 @@ class res_users(osv.Model):
self._create_welcome_message(cr, uid, user, context=context)
return user_id
+ def copy_data(self, *args, **kwargs):
+ data = super(res_users, self).copy_data(*args, **kwargs)
+ if data.get('alias_name'):
+ data['alias_name'] = data['login']
+ return data
+
def _create_welcome_message(self, cr, uid, user, context=None):
if not self.has_group(cr, uid, 'base.group_user'):
return False
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index b2a1c3250aa..c88ed6555d6 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -163,7 +163,7 @@ class TestMailgateway(TestMail):
self.assertIn('
Should create a multipart/mixed: from gmail,
bold, with attachment.
', res.get('body', ''),
'message_parse: html version should be in body after parsing multipart/mixed')
- @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
+ # @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_10_message_process(self):
""" Testing incoming emails processing. """
cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
@@ -378,7 +378,7 @@ class TestMailgateway(TestMail):
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
to='erroneous@example.com>', subject='Re: news',
- extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
+ extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>\n')
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
@@ -387,16 +387,41 @@ class TestMailgateway(TestMail):
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: one new message
- self.assertEqual(len(frog_group.message_ids), 3, 'message_process: group should contain 2 messages after reply')
+ self.assertEqual(len(frog_group.message_ids), 3, 'message_process: group should contain 3 messages after reply')
# Test: author (and not recipient) added as follower
frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
self.assertEqual(frog_follower_ids, set([p1id, p2id]),
'message_process: after reply, group should have 2 followers')
+ # Do: incoming email with ref holding model / res_id but that does not match any message in the thread: must raise since OpenERP saas-3
+ self.assertRaises(AssertionError,
+ format_and_process,
+ MAIL_TEMPLATE, email_from='other5@gmail.com',
+ to='noone@example.com', subject='spam',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id,
+ msg_id='<1.1.JavaMail.new@agrolait.com>')
+ # There are 6.1 messages, activate compat mode
+ tmp_msg_id = self.mail_message.create(cr, uid, {'message_id': False, 'model': 'mail.group', 'res_id': frog_group.id})
+ # Do: compat mode accepts partial-matching emails
+ frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other5@gmail.com',
+ msg_id='<1.2.JavaMail.new@agrolait.com>',
+ to='noone@example.com>', subject='spam',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id)
+ self.mail_message.unlink(cr, uid, [tmp_msg_id])
+ # Test: no group 'Re: news' created, still only 1 Frogs group
+ self.assertEqual(len(frog_groups), 0,
+ 'message_process: reply on Frogs should not have created a new group with new subject')
+ frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
+ self.assertEqual(len(frog_groups), 1,
+ 'message_process: reply on Frogs should not have created a duplicate group with old subject')
+ frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
+ # Test: one new message
+ self.assertEqual(len(frog_group.message_ids), 4, 'message_process: group should contain 4 messages after reply')
+
# Do: due to some issue, same email goes back into the mailgateway
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
- subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
+ subject='Re: news', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>\n')
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
@@ -405,7 +430,7 @@ class TestMailgateway(TestMail):
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: no new message
- self.assertEqual(len(frog_group.message_ids), 3, 'message_process: message with already existing message_id should not have been duplicated')
+ self.assertEqual(len(frog_group.message_ids), 4, 'message_process: message with already existing message_id should not have been duplicated')
# Test: message_id is still unique
msg_ids = self.mail_message.search(cr, uid, [('message_id', 'ilike', '<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>')])
self.assertEqual(len(msg_ids), 1,
@@ -422,7 +447,7 @@ class TestMailgateway(TestMail):
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik
',
to='erroneous@example.com>', subject='Re: news (2)',
msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
- extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
+ extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is A-Raoul (only existing)
@@ -436,7 +461,7 @@ class TestMailgateway(TestMail):
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ',
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
- extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
+ extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is Raoul (user), not A-Raoul
@@ -451,7 +476,7 @@ class TestMailgateway(TestMail):
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ',
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
- extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
+ extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is Raoul (user), not A-Raoul
@@ -498,7 +523,7 @@ class TestMailgateway(TestMail):
self.assertIn('\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
', msg.body,
'message_process: plaintext incoming email incorrectly parsed')
- @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
+ # @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_20_thread_parent_resolution(self):
""" Testing parent/child relationships are correctly established when processing incoming mails """
cr, uid = self.cr, self.uid
diff --git a/addons/portal_sale/portal_sale_data.xml b/addons/portal_sale/portal_sale_data.xml
index 750ae12995f..2da55bcb081 100644
--- a/addons/portal_sale/portal_sale_data.xml
+++ b/addons/portal_sale/portal_sale_data.xml
@@ -13,6 +13,7 @@
${(object.name or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}
+ ${object.partner_id.lang}
@@ -102,6 +103,7 @@
Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}
+ ${object.partner_id.lang}
diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py
index 2368a9849de..88b4b14a36a 100644
--- a/addons/sale/report/sale_report.py
+++ b/addons/sale/report/sale_report.py
@@ -27,6 +27,7 @@ class sale_report(osv.osv):
_description = "Sales Orders Statistics"
_auto = False
_rec_name = 'date'
+
_columns = {
'date': fields.date('Date Order', readonly=True),
'date_confirm': fields.date('Date Confirm', readonly=True),
@@ -60,12 +61,9 @@ class sale_report(osv.osv):
}
_order = 'date desc'
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'sale_report')
- cr.execute("""
- create or replace view sale_report as (
- select
- min(l.id) as id,
+ def _select(self):
+ select_str = """
+ SELECT min(l.id) as id,
l.product_id as product_id,
t.uom_id as product_uom,
sum(l.product_uom_qty / u.factor * u2.factor) as product_uom_qty,
@@ -84,15 +82,23 @@ class sale_report(osv.osv):
t.categ_id as categ_id,
s.pricelist_id as pricelist_id,
s.project_id as analytic_account_id
- from
- sale_order_line l
+ """
+ return select_str
+
+ def _from(self):
+ from_str = """
+ sale_order_line l
join sale_order s on (l.order_id=s.id)
left join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
left join product_uom u on (u.id=l.product_uom)
left join product_uom u2 on (u2.id=t.uom_id)
- group by
- l.product_id,
+ """
+ return from_str
+
+ def _group_by(self):
+ group_by_str = """
+ GROUP BY l.product_id,
l.order_id,
t.uom_id,
t.categ_id,
@@ -104,7 +110,16 @@ class sale_report(osv.osv):
s.state,
s.pricelist_id,
s.project_id
- )
- """)
+ """
+ return group_by_str
+
+ def init(self, cr):
+ # self._table = sale_report
+ tools.drop_view_if_exists(cr, self._table)
+ cr.execute("""CREATE or REPLACE VIEW %s as (
+ %s
+ FROM ( %s )
+ %s
+ )""" % (self._table, self._select(), self._from(), self._group_by()))
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale/report/sale_report_view.xml b/addons/sale/report/sale_report_view.xml
index 69e9cc5e119..28a6f83936a 100644
--- a/addons/sale/report/sale_report_view.xml
+++ b/addons/sale/report/sale_report_view.xml
@@ -47,10 +47,8 @@
-
-
-
-
+
+
diff --git a/addons/sale/security/sale_security.xml b/addons/sale/security/sale_security.xml
index 557562275d7..c34f29fc81b 100644
--- a/addons/sale/security/sale_security.xml
+++ b/addons/sale/security/sale_security.xml
@@ -78,6 +78,13 @@
['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
+
+ Sales Order Analysis multi-company
+
+
+ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
+
+
@@ -93,6 +100,19 @@
+
+ Personal Orders Analysis
+
+ ['|',('user_id','=',user.id),('user_id','=',False)]
+
+
+
+
+ All Orders Analysis
+
+ [(1,'=',1)]
+
+
Personal Order Lines
diff --git a/addons/sale_crm/__openerp__.py b/addons/sale_crm/__openerp__.py
index 7a57484eab8..9c227e39430 100644
--- a/addons/sale_crm/__openerp__.py
+++ b/addons/sale_crm/__openerp__.py
@@ -46,6 +46,7 @@ modules.
'security/sale_crm_security.xml',
'security/ir.model.access.csv',
'report/sale_crm_account_invoice_report_view.xml',
+ 'report/sale_report_view.xml',
],
'js': [
'static/src/js/sale_crm.js',
diff --git a/addons/sale_crm/report/__init__.py b/addons/sale_crm/report/__init__.py
index d7c6d373468..d86b3e66de9 100644
--- a/addons/sale_crm/report/__init__.py
+++ b/addons/sale_crm/report/__init__.py
@@ -20,6 +20,7 @@
##############################################################################
import sales_crm_account_invoice_report
+import sale_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale_crm/report/sale_report.py b/addons/sale_crm/report/sale_report.py
new file mode 100644
index 00000000000..eca020f82ad
--- /dev/null
+++ b/addons/sale_crm/report/sale_report.py
@@ -0,0 +1,18 @@
+ # -*- coding: utf-8 -*-
+
+from openerp.osv import fields, osv
+
+
+class sale_report(osv.osv):
+ _inherit = "sale.report"
+ _columns = {
+ 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
+ }
+
+ def _select(self):
+ return super(sale_report, self)._select() + ", s.section_id as section_id"
+
+ def _group_by(self):
+ return super(sale_report, self)._group_by() + ", s.section_id"
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale_crm/report/sale_report_view.xml b/addons/sale_crm/report/sale_report_view.xml
new file mode 100644
index 00000000000..0d8243c1275
--- /dev/null
+++ b/addons/sale_crm/report/sale_report_view.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ sale.report.tree.sale.crm
+ sale.report
+
+
+
+
+
+
+
+
+
+ sale.report.search.sale.crm
+ sale.report
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/sale_crm/sale_crm.py b/addons/sale_crm/sale_crm.py
index e5695daab09..f1370841fe9 100644
--- a/addons/sale_crm/sale_crm.py
+++ b/addons/sale_crm/sale_crm.py
@@ -53,10 +53,10 @@ class crm_case_section(osv.osv):
date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
res[id] = dict()
- created_domain = [('section_id', '=', id), ('state', 'in', ['draft', 'sent']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)]
+ created_domain = [('section_id', '=', id), ('state', '=', ['draft']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)]
res[id]['monthly_quoted'] = self.__get_bar_values(cr, uid, obj, created_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context)
- validated_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'sent']), ('date_confirm', '>=', date_begin), ('date_order', '<=', date_end)]
- res[id]['monthly_confirmed'] = self.__get_bar_values(cr, uid, obj, validated_domain, ['amount_total', 'date_confirm'], 'amount_total', 'date_confirm', context=context)
+ validated_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'sent', 'cancel']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)]
+ res[id]['monthly_confirmed'] = self.__get_bar_values(cr, uid, obj, validated_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context)
return res
def _get_invoices_data(self, cr, uid, ids, field_name, arg, context=None):
@@ -90,7 +90,7 @@ class crm_case_section(osv.osv):
}
def action_forecast(self, cr, uid, id, value, context=None):
- return self.write(cr, uid, [id], {'invoiced_forecast': value}, context=context)
+ return self.write(cr, uid, [id], {'invoiced_forecast': round(float(value))}, context=context)
class res_users(osv.Model):
diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml
index 8c008339903..8c3c98c3f97 100644
--- a/addons/sale_crm/sale_crm_view.xml
+++ b/addons/sale_crm/sale_crm_view.xml
@@ -211,6 +211,36 @@
+
+ Quotations Analysis
+ sale.report
+ form
+ tree,graph
+ [('state','=','draft'),('section_id', '=', active_id)]
+ {'search_default_order_month':1}
+ This report performs analysis on your quotations. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.
+
+
+
+ Sales Analysis
+ sale.report
+ form
+ tree,graph
+ [('state','not in',('draft','sent','cancel')),('section_id', '=', active_id)]
+ {'search_default_order_month':1}
+ This report performs analysis on your sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.
+
+
+
+ Invoices Analysis
+ account.invoice.report
+ form
+ tree,graph
+ [('section_id', '=', active_id),('state', 'not in', ['draft', 'cancel'])]
+ {'search_default_month':1}
+ From this report, you can have an overview of the amount invoiced to your customer. The tool search can also be used to personalise your Invoices reports and so, match this analysis to your needs.
+
+
crm.case.section.form
crm.case.section
@@ -241,9 +271,9 @@
@@ -251,27 +281,29 @@
-
+
- Invoiced
- Forecast
+ Invoiced
+ Forecast
-
+
Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance.
diff --git a/addons/sale_stock/report/sale_report.py b/addons/sale_stock/report/sale_report.py
index bdcce410a5f..7a8234fa7fa 100644
--- a/addons/sale_stock/report/sale_report.py
+++ b/addons/sale_stock/report/sale_report.py
@@ -39,59 +39,11 @@ class sale_report(osv.osv):
('cancel', 'Cancelled')
], 'Order Status', readonly=True),
}
-
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'sale_report')
- # TODO: make parent view extensible similarly to invoice analysis and
- # remove the duplication
- cr.execute("""
- create or replace view sale_report as (
- select
- min(l.id) as id,
- l.product_id as product_id,
- t.uom_id as product_uom,
- sum(l.product_uom_qty / u.factor * u2.factor) as product_uom_qty,
- sum(l.product_uom_qty * l.price_unit * (100.0-l.discount) / 100.0) as price_total,
- count(*) as nbr,
- s.date_order as date,
- s.date_confirm as date_confirm,
- to_char(s.date_order, 'YYYY') as year,
- to_char(s.date_order, 'MM') as month,
- to_char(s.date_order, 'YYYY-MM-DD') as day,
- s.partner_id as partner_id,
- s.user_id as user_id,
- s.company_id as company_id,
- s.warehouse_id as warehouse_id,
- extract(epoch from avg(date_trunc('day',s.date_confirm)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay,
- s.state,
- t.categ_id as categ_id,
- s.shipped,
- s.shipped::integer as shipped_qty_1,
- s.pricelist_id as pricelist_id,
- s.project_id as analytic_account_id
- from
- sale_order_line l
- join sale_order s on (l.order_id=s.id)
- left join product_product p on (l.product_id=p.id)
- left join product_template t on (p.product_tmpl_id=t.id)
- left join product_uom u on (u.id=l.product_uom)
- left join product_uom u2 on (u2.id=t.uom_id)
- group by
- l.product_id,
- l.order_id,
- t.uom_id,
- t.categ_id,
- s.date_order,
- s.date_confirm,
- s.partner_id,
- s.user_id,
- s.warehouse_id,
- s.company_id,
- s.state,
- s.shipped,
- s.pricelist_id,
- s.project_id
- )
- """)
+
+ def _select(self):
+ return super(sale_report, self)._select() + ", s.warehouse_id as warehouse_id, s.shipped, s.shipped::integer as shipped_qty_1"
+
+ def _group_by(self):
+ return super(sale_report, self)._group_by() + ", s.warehouse_id, s.shipped"
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale_stock/report/sale_report_view.xml b/addons/sale_stock/report/sale_report_view.xml
index 6642bffcede..486cf3a7b11 100644
--- a/addons/sale_stock/report/sale_report_view.xml
+++ b/addons/sale_stock/report/sale_report_view.xml
@@ -17,6 +17,10 @@
sale.report
+
+
+
+
diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py
index 341aaef8ca8..26d9d80f13e 100644
--- a/addons/sale_stock/sale_stock.py
+++ b/addons/sale_stock/sale_stock.py
@@ -70,7 +70,7 @@ class sale_order(osv.osv):
company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
if not warehouse_ids:
- raise osv.except_osv(_('Error!'), _('There is no warehouse defined for current company.'))
+ return False
return warehouse_ids[0]
# This is False