+To: {to}
+Received: by mail1.openerp.com (Postfix, from userid 10002)
+ id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
+From: {email_from}
+Subject: {subject}
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="----=_Part_4200734_24778174.1344608186754"
+Date: Fri, 10 Aug 2012 14:16:26 +0000
+Message-ID: {msg_id}
+{extra}
+------=_Part_4200734_24778174.1344608186754
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+Please call me as soon as possible this afternoon!
+
+--
+Sylvie
+------=_Part_4200734_24778174.1344608186754
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+
+
+ =20
+
+ =20
+ =20
+
+ Please call me as soon as possible this afternoon!
+
+ --
+ Sylvie
+
+
+
+------=_Part_4200734_24778174.1344608186754--
+"""
+
+MAIL_TEMPLATE_PLAINTEXT = """Return-Path:
+To: {to}
+Received: by mail1.openerp.com (Postfix, from userid 10002)
+ id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
+From: Sylvie Lelitre
+Subject: {subject}
+MIME-Version: 1.0
+Content-Type: text/plain
+Date: Fri, 10 Aug 2012 14:16:26 +0000
+Message-ID: {msg_id}
+{extra}
+
+Please call me as soon as possible this afternoon!
+
+--
+Sylvie
+"""
+
+
+class TestMailgateway(TestMailBase):
+
+ def test_00_message_process(self):
+ """ Testing incoming emails processing. """
+ cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
+
+ def format_and_process(template, to='groups@example.com, other@gmail.com', subject='Frogs',
+ extra='', email_from='Sylvie Lelitre ',
+ msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
+ model=None):
+ self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', subject)]), [])
+ mail = template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
+ self.mail_thread.message_process(cr, uid, model, mail)
+ return self.mail_group.search(cr, uid, [('name', '=', subject)])
+
+ # --------------------------------------------------
+ # Data creation
+ # --------------------------------------------------
+
+ # groups@.. will cause the creation of new mail groups
+ self.mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
+ alias_id = self.mail_alias.create(cr, uid, {
+ 'alias_name': 'groups',
+ 'alias_user_id': False,
+ 'alias_model_id': self.mail_group_model_id})
+
+ # --------------------------------------------------
+ # Test1: new record creation
+ # --------------------------------------------------
+
+ # Do: incoming mail from an unknown partner on an alias creates a new mail_group "frogs"
+ self._init_mock_build_email()
+ frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com')
+ sent_emails = self._build_email_kwargs_list
+ # Test: one group created by mailgateway administrator
+ self.assertTrue(len(frog_groups) == 1)
+ frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
+ # Test: one message that is the incoming email
+ self.assertEqual(len(frog_group.message_ids), 1,
+ 'message_process: newly created group should have the incoming email in message_ids')
+ msg = frog_group.message_ids[0]
+ self.assertEqual('Frogs', msg.subject,
+ 'message_process: newly created group should have the incoming email as first message')
+ self.assertIn('Please call me as soon as possible this afternoon!', msg.body,
+ 'message_process: newly created group should have the incoming email as first message')
+ self.assertEqual('email', msg.type,
+ 'message_process: newly created group should have an email as first message')
+ self.assertEqual('Discussions', msg.subtype_id.name,
+ 'message_process: newly created group should not have a log first message but an email')
+ # Test: message: unknown email address -> message has email_from, not author_id
+ self.assertFalse(msg.author_id,
+ 'message_process: message on created group should not have an author_id')
+ self.assertIn('test.sylvie.lelitre@agrolait.com', msg.email_from,
+ 'message_process: message on created group should have an email_from')
+ # Test: followers: nobody
+ self.assertEqual(len(frog_group.message_follower_ids), 0, 'message_process: newly create group should not have any follower')
+ # Test: sent emails: no-one
+ self.assertEqual(len(sent_emails), 0,
+ 'message_process: should create emails without any follower added')
+ # Data: unlink group
+ frog_group.unlink()
+
+ # Do: incoming email from a known partner on an alias with known recipients, alias is owned by user that can create a group
+ self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id})
+ p1id = self.res_partner.create(cr, uid, {'name': 'Sylvie Lelitre', 'email': 'test.sylvie.lelitre@agrolait.com'})
+ p2id = self.res_partner.create(cr, uid, {'name': 'Other Poilvache', 'email': 'other@gmail.com'})
+ self._init_mock_build_email()
+ frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com')
+ sent_emails = self._build_email_kwargs_list
+ # Test: one group created by raoul
+ self.assertTrue(len(frog_groups) == 1)
+ frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
+ # Test: one message that is the incoming email
+ self.assertEqual(len(frog_group.message_ids), 1,
+ 'message_process: newly created group should have the incoming email in message_ids')
+ msg = frog_group.message_ids[0]
+ # Test: message: unknown email address -> message has email_from, not author_id
+ self.assertEqual(p1id, msg.author_id.id,
+ 'message_process: message on created group should have Sylvie as author_id')
+ self.assertIn('Sylvie Lelitre ', msg.email_from,
+ 'message_process: message on created group should have have an email_from')
+ # Test: author (not recipient and and not raoul (as alias owner)) added as follower
+ frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
+ self.assertEqual(frog_follower_ids, set([p1id]),
+ 'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
+ # Test: sent emails: no-one, no bounce effet
+ self.assertEqual(len(sent_emails), 0,
+ 'message_process: should not bounce incoming emails')
+ # Data: unlink group
+ frog_group.unlink()
+
+ # Do: incoming email from a known partner that is also an user that can create a mail.group
+ self.res_users.create(cr, uid, {'partner_id': p1id, 'login': 'sylvie', 'groups_id': [(6, 0, [self.group_employee_id])]})
+ frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com')
+ # Test: one group created by Sylvie
+ self.assertTrue(len(frog_groups) == 1)
+ frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
+ # Test: one message that is the incoming email
+ self.assertEqual(len(frog_group.message_ids), 1,
+ 'message_process: newly created group should have the incoming email in message_ids')
+ # 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]),
+ 'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
+ # Test: sent emails: no-one, no bounce effet
+ self.assertEqual(len(sent_emails), 0,
+ 'message_process: should not bounce incoming emails')
+
+ # --------------------------------------------------
+ # Test2: discussion update
+ # --------------------------------------------------
+
+ # Do: even with a wrong destination, a reply should end up in the correct thread
+ frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com',
+ to='erroneous@example.com>', subject='Re: news',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.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.assertTrue(len(frog_group.message_ids) == 2, 'message_process: group should contain 2 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')
+
+ # --------------------------------------------------
+ # Test3: misc gateway features
+ # --------------------------------------------------
+
+ # Do: incoming email with model that does not accepts incoming emails must raise
+ self.assertRaises(AssertionError,
+ format_and_process,
+ MAIL_TEMPLATE, to='noone@example.com', subject='spam', extra='', model='res.country')
+
+ # Do: incoming email without model and without alias must raise
+ self.assertRaises(AssertionError,
+ format_and_process,
+ MAIL_TEMPLATE, to='noone@example.com', subject='spam', extra='')
+
+ # Do: incoming email with model that accepting incoming emails as fallback
+ frog_groups = format_and_process(MAIL_TEMPLATE, to='noone@example.com', subject='Spammy', extra='', model='mail.group')
+ self.assertEqual(len(frog_groups), 1,
+ 'message_process: erroneous email but with a fallback model should have created a new mail.group')
+
+ # Do: incoming email in plaintext should be stored as html
+ frog_groups = format_and_process(MAIL_TEMPLATE_PLAINTEXT, to='groups@example.com', subject='Frogs Return', extra='', msg_id='')
+ # Test: one group created with one message
+ self.assertTrue(len(frog_groups) == 1)
+ frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
+ msg = frog_group.message_ids[0]
+ # Test: plain text content should be wrapped and stored as html
+ self.assertEqual(msg.body, '\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n ',
+ 'message_process: plaintext incoming email incorrectly parsed')
+
+ def test_10_thread_parent_resolution(self):
+ """ Testing parent/child relationships are correctly established when processing incoming mails """
+ cr, uid = self.cr, self.uid
+
+ def format(template, to='Pretty Pigs , other@gmail.com', subject='Re: 1',
+ extra='', email_from='Sylvie Lelitre ',
+ msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>'):
+ return template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
+
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ msg1 = group_pigs.message_post(body='My Body', subject='1')
+ msg2 = group_pigs.message_post(body='My Body', subject='2')
+ msg1, msg2 = self.mail_message.browse(cr, uid, [msg1, msg2])
+ self.assertTrue(msg1.message_id, "message_process: new message should have a proper message_id")
+
+ # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
+ # 0. Direct alias match
+ reply_msg1 = format(MAIL_TEMPLATE, to='Pretty Pigs ', extra='In-Reply-To: %s' % msg1.message_id)
+ self.mail_group.message_process(cr, uid, None, reply_msg1)
+
+ # 1. In-Reply-To header
+ reply_msg2 = format(MAIL_TEMPLATE, to='erroneous@example.com', extra='In-Reply-To: %s' % msg1.message_id)
+ self.mail_group.message_process(cr, uid, None, reply_msg2)
+
+ # 2. References header
+ reply_msg3 = format(MAIL_TEMPLATE, to='erroneous@example.com', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
+ self.mail_group.message_process(cr, uid, None, reply_msg3)
+
+ # 3. Subject contains [] + model passed to message+process -> only attached to group, but not to mail (not in msg1.child_ids)
+ reply_msg4 = format(MAIL_TEMPLATE, to='erroneous@example.com', extra='', subject='Re: [%s] 1' % self.group_pigs_id)
+ self.mail_group.message_process(cr, uid, 'mail.group', reply_msg4)
+
+ group_pigs.refresh()
+ msg1.refresh()
+ self.assertEqual(6, len(group_pigs.message_ids), 'message_process: group should contain 6 messages')
+ self.assertEqual(3, len(msg1.child_ids), 'message_process: msg1 should have 3 children now')
+
+ def test_20_private_discussion(self):
+ """ Testing private discussion between partners. """
+ pass
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 5622d723eb9..0442edbbaba 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -23,6 +23,7 @@ import base64
import re
from openerp import tools
+from openerp import SUPERUSER_ID
from openerp.osv import osv
from openerp.osv import fields
from openerp.tools.safe_eval import safe_eval as eval
@@ -136,6 +137,29 @@ class mail_compose_message(osv.TransientModel):
'same_thread': lambda self, cr, uid, ctx={}: True,
}
+ def check_access_rule(self, cr, uid, ids, operation, context=None):
+ """ Access rules of mail.compose.message:
+ - create: if
+ - model, no res_id, I create a message in mass mail mode
+ - then: fall back on mail.message acces rules
+ """
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+
+ # Author condition (CREATE (mass_mail))
+ if operation == 'create' and uid != SUPERUSER_ID:
+ # read mail_compose_message.ids to have their values
+ message_values = {}
+ cr.execute('SELECT DISTINCT id, model, res_id FROM "%s" WHERE id = ANY (%%s) AND res_id = 0' % self._table, (ids,))
+ for id, rmod, rid in cr.fetchall():
+ message_values[id] = {'model': rmod, 'res_id': rid}
+ # remove from the set to check the ids that mail_compose_message accepts
+ author_ids = [mid for mid, message in message_values.iteritems()
+ if message.get('model') and not message.get('res_id')]
+ ids = list(set(ids) - set(author_ids))
+
+ return super(mail_compose_message, self).check_access_rule(cr, uid, ids, operation, context=context)
+
def _notify(self, cr, uid, newid, context=None):
""" Override specific notify method of mail.message, because we do
not want that feature in the wizard. """
@@ -244,8 +268,11 @@ class mail_compose_message(osv.TransientModel):
self.pool.get('mail.mail').create(cr, uid, post_values, context=context)
else:
subtype = 'mail.mt_comment'
- if is_log or (mass_mail_mode and not wizard.notify):
+ if is_log: # log a note: subtype is False
subtype = False
+ elif mass_mail_mode: # mass mail: is a log pushed to recipients, author not added
+ subtype = False
+ context = dict(context, mail_create_nosubscribe=True) # add context key to avoid subscribing the author
msg_id = active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
# mass_mailing, post without notify: notify specific partners
if mass_mail_mode and not wizard.notify and post_values['partner_ids']:
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index f3da234463c..2af1e876c70 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -701,11 +701,6 @@
-
@@ -724,10 +719,6 @@
string="Partial"
type="action" states="confirmed,assigned"
icon="gtk-justify-fill"/>
-
diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py
index af5d7aa663c..7e8487b2b2b 100644
--- a/addons/point_of_sale/point_of_sale.py
+++ b/addons/point_of_sale/point_of_sale.py
@@ -89,7 +89,7 @@ class pos_config(osv.osv):
'sequence_id' : False,
}
d.update(default)
- return super(pos_order, self).copy(cr, uid, id, d, context=context)
+ return super(pos_config, self).copy(cr, uid, id, d, context=context)
def name_get(self, cr, uid, ids, context=None):
diff --git a/addons/point_of_sale/wizard/pos_box.py b/addons/point_of_sale/wizard/pos_box.py
index 14e653b915c..776d962a3a8 100644
--- a/addons/point_of_sale/wizard/pos_box.py
+++ b/addons/point_of_sale/wizard/pos_box.py
@@ -31,6 +31,10 @@ class PosBoxIn(PosBox):
_inherit = 'cash.box.in'
def _compute_values_for_statement_line(self, cr, uid, box, record, context=None):
+
+ if context is None:
+ context = {}
+
values = super(PosBoxIn, self)._compute_values_for_statement_line(cr, uid, box, record, context=context)
active_model = context.get('active_model', False) or False
diff --git a/addons/portal_anonymous/static/src/js/portal_anonymous.js b/addons/portal_anonymous/static/src/js/portal_anonymous.js
index a719a285908..d4f6351efc9 100644
--- a/addons/portal_anonymous/static/src/js/portal_anonymous.js
+++ b/addons/portal_anonymous/static/src/js/portal_anonymous.js
@@ -43,8 +43,12 @@ openerp.portal_anonymous = function(instance) {
instance.web.Login.include({
start: function() {
var self = this;
+ var anonymous_mode = (!self.session.session_is_valid() && !(self.params.token || self.params.login));
+ if (anonymous_mode) {
+ self.$el.hide();
+ }
return $.when(this._super()).then(function() {
- if (!self.session.session_is_valid() && !(self.params.token || self.params.login)) {
+ if (anonymous_mode) {
self.remember_credentials = false;
// XXX get login/pass from server (via a rpc call) ?
return self.do_login(self.selected_db, 'anonymous', 'anonymous');
diff --git a/addons/portal_project_long_term/__init__.py b/addons/portal_project_long_term/__init__.py
new file mode 100644
index 00000000000..c8d82ed19bd
--- /dev/null
+++ b/addons/portal_project_long_term/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP S.A. ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
diff --git a/addons/portal_project_long_term/__openerp__.py b/addons/portal_project_long_term/__openerp__.py
new file mode 100644
index 00000000000..44156afbc06
--- /dev/null
+++ b/addons/portal_project_long_term/__openerp__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP S.A. ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+
+{
+ 'name': 'Portal Project Long Term',
+ 'version': '1.0',
+ 'category': 'Tools',
+ 'complexity': 'easy',
+ 'description': """
+This module adds necessary security rules and access rights for project long term and portal.
+=============================================================================================
+ """,
+ 'author': 'OpenERP SA',
+ 'depends': ['project_long_term', 'portal'],
+ 'data': [
+ 'security/portal_security.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+ 'category': 'Hidden',
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/portal_project_long_term/security/ir.model.access.csv b/addons/portal_project_long_term/security/ir.model.access.csv
new file mode 100644
index 00000000000..738c264a71c
--- /dev/null
+++ b/addons/portal_project_long_term/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_issues,project_phase,project_long_term.model_project_phase,portal.group_portal,1,0,0,0
diff --git a/addons/portal_project_long_term/security/portal_security.xml b/addons/portal_project_long_term/security/portal_security.xml
new file mode 100644
index 00000000000..b46d277e4ee
--- /dev/null
+++ b/addons/portal_project_long_term/security/portal_security.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Portal Personal Long term project
+
+ ['|',('project_id.message_follower_ids','in', [user.partner_id.id]),('task_ids.message_follower_ids','in', [user.partner_id.id])]
+
+
+
+
+
+
+
+
+
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py
index 9233a95634c..01dab2962f3 100644
--- a/addons/project_issue/project_issue.py
+++ b/addons/project_issue/project_issue.py
@@ -392,10 +392,18 @@ class project_issue(base_stage, osv.osv):
context=context)
def write(self, cr, uid, ids, vals, context=None):
- #Update last action date every time the user change the stage, the state or send a new email
- logged_fields = ['stage_id', 'state', 'message_ids']
- if any([field in vals for field in logged_fields]):
- vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
+
+ #Update last action date every time the user changes the stage
+ if 'stage_id' in vals:
+ vals['date_action_last'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ state = self.pool.get('project.task.type').browse(cr, uid, vals['stage_id'], context=context).state
+ for issue in self.browse(cr, uid, ids, context=context):
+ # Change from draft to not draft EXCEPT cancelled: The issue has been opened -> set the opening date
+ if issue.state == 'draft' and state not in ('draft', 'cancelled'):
+ vals['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ # Change from not done to done: The issue has been closed -> set the closing date
+ if issue.state != 'done' and state == 'done':
+ vals['date_closed'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
return super(project_issue, self).write(cr, uid, ids, vals, context)
@@ -545,6 +553,19 @@ class project_issue(base_stage, osv.osv):
return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
+ def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs):
+ """ Overrides mail_thread message_post so that we can set the date of last action field when
+ a new message is posted on the issue.
+ """
+ if context is None:
+ context = {}
+
+ res = super(project_issue, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
+
+ if thread_id:
+ self.write(cr, uid, thread_id, {'date_action_last': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
+
+ return res
class project(osv.osv):
_inherit = "project.project"
diff --git a/addons/sale/report/sale_order.py b/addons/sale/report/sale_order.py
index b4f0b1589aa..134e87a0c2e 100644
--- a/addons/sale/report/sale_order.py
+++ b/addons/sale/report/sale_order.py
@@ -27,9 +27,18 @@ class order(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context=None):
super(order, self).__init__(cr, uid, name, context=context)
self.localcontext.update({
- 'time': time,
+ 'time': time,
+ 'show_discount':self._show_discount,
})
+ def _show_discount(self, uid, context=None):
+ cr = self.cr
+ try:
+ group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'group_discount_per_so_line')[1]
+ except:
+ return False
+ return group_id in [x.id for x in self.pool.get('res.users').browse(cr, uid, uid, context=context).groups_id]
+
report_sxw.report_sxw('report.sale.order', 'sale.order', 'addons/sale/report/sale_order.rml', parser=order, header="external")
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale/report/sale_order.rml b/addons/sale/report/sale_order.rml
index a00d2d4e3ef..130220535f9 100644
--- a/addons/sale/report/sale_order.rml
+++ b/addons/sale/report/sale_order.rml
@@ -251,7 +251,7 @@
Unit Price
- Disc.(%)
+ [[not show_discount(user.id) and removeParentNode('para') ]]Disc.(%)
Price
@@ -275,7 +275,7 @@
[[ formatLang(line.price_unit , digits=get_digits(dp='Product Price'))]]
- [[ formatLang(line.discount, digits=get_digits(dp='Discount'))]]
+ [[show_discount(user.id) and formatLang(line.discount, digits=get_digits(dp='Discount')) or '']]
[[ formatLang(line.price_subtotal, digits=get_digits(dp='Account'), currency_obj=o.pricelist_id.currency_id) ]]
diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py
index a20ba01f300..070e9c4336f 100644
--- a/addons/sale/report/sale_report.py
+++ b/addons/sale/report/sale_report.py
@@ -88,12 +88,11 @@ class sale_report(osv.osv):
s.project_id as analytic_account_id
from
sale_order s
- left join sale_order_line l on (s.id=l.order_id)
+ join sale_order_line l on (s.id=l.order_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)
- where l.product_id is not null
group by
l.product_id,
l.product_uom_qty,
diff --git a/addons/sale_stock/report/sale_report.py b/addons/sale_stock/report/sale_report.py
index ade84b269e6..1f1e4a625be 100644
--- a/addons/sale_stock/report/sale_report.py
+++ b/addons/sale_stock/report/sale_report.py
@@ -68,8 +68,8 @@ class sale_report(osv.osv):
s.project_id as analytic_account_id
from
sale_order s
- left join sale_order_line l on (s.id=l.order_id)
- left join product_product p on (l.product_id=p.id)
+ join sale_order_line l on (s.id=l.order_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)
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 6bf020623f7..ba3db96ff68 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -19,7 +19,6 @@
#
##############################################################################
-from lxml import etree
from datetime import datetime
from dateutil.relativedelta import relativedelta
import time
@@ -704,29 +703,29 @@ class stock_picking(osv.osv):
default = {}
default = default.copy()
picking_obj = self.browse(cr, uid, id, context=context)
- move_obj=self.pool.get('stock.move')
- if ('name' not in default) or (picking_obj.name=='/'):
- seq_obj_name = 'stock.picking.' + picking_obj.type
+ move_obj = self.pool.get('stock.move')
+ if ('name' not in default) or (picking_obj.name == '/'):
+ seq_obj_name = 'stock.picking.' + picking_obj.type
default['name'] = self.pool.get('ir.sequence').get(cr, uid, seq_obj_name)
default['origin'] = ''
default['backorder_id'] = False
if 'invoice_state' not in default and picking_obj.invoice_state == 'invoiced':
default['invoice_state'] = '2binvoiced'
- res=super(stock_picking, self).copy(cr, uid, id, default, context)
+ res = super(stock_picking, self).copy(cr, uid, id, default, context)
if res:
picking_obj = self.browse(cr, uid, res, context=context)
for move in picking_obj.move_lines:
- move_obj.write(cr, uid, [move.id], {'tracking_id': False,'prodlot_id':False, 'move_history_ids2': [(6, 0, [])], 'move_history_ids': [(6, 0, [])]})
+ move_obj.write(cr, uid, [move.id], {'tracking_id': False, 'prodlot_id': False, 'move_history_ids2': [(6, 0, [])], 'move_history_ids': [(6, 0, [])]})
return res
-
+
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
if view_type == 'form' and not view_id:
mod_obj = self.pool.get('ir.model.data')
if self._name == "stock.picking.in":
- model,view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_in_form')
+ model, view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_in_form')
if self._name == "stock.picking.out":
- model,view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
- return super(stock_picking,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
+ model, view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
+ return super(stock_picking, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
def onchange_partner_in(self, cr, uid, ids, partner_id=None, context=None):
return {}
@@ -2440,9 +2439,8 @@ class stock_move(osv.osv):
context = {}
ctx = context.copy()
for move in self.browse(cr, uid, ids, context=context):
- if move.state != 'draft' and not ctx.get('call_unlink',False):
- raise osv.except_osv(_('User Error!'),
- _('You can only delete draft moves.'))
+ if move.state != 'draft' and not ctx.get('call_unlink', False):
+ raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
return super(stock_move, self).unlink(
cr, uid, ids, context=ctx)
@@ -2470,13 +2468,20 @@ class stock_move(osv.osv):
raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
res = []
for move in self.browse(cr, uid, ids, context=context):
+ source_location = move.location_id
+ if move.state == 'done':
+ source_location = move.location_dest_id
+ if source_location.usage != 'internal':
+ #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
+ raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
move_qty = move.product_qty
uos_qty = quantity / move_qty * move.product_uos_qty
default_val = {
+ 'location_id': source_location.id,
'product_qty': quantity,
'product_uos_qty': uos_qty,
'state': move.state,
- 'scrapped' : True,
+ 'scrapped': True,
'location_dest_id': location_id,
'tracking_id': move.tracking_id.id,
'prodlot_id': move.prodlot_id.id,
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index bc47d83dc89..6162d073fde 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -1129,7 +1129,7 @@
stock.move
-
+
diff --git a/addons/subscription/subscription.py b/addons/subscription/subscription.py
index d181e97e8f0..92bde135bc6 100644
--- a/addons/subscription/subscription.py
+++ b/addons/subscription/subscription.py
@@ -85,6 +85,17 @@ class subscription_subscription(osv.osv):
'state': lambda *a: 'draft'
}
+ def _auto_end(self, cr, context=None):
+ super(subscription_subscription, self)._auto_end(cr, context=context)
+ # drop the FK from subscription to ir.cron, as it would cause deadlocks
+ # during cron job execution. When model_copy() tries to write() on the subscription,
+ # it has to wait for an ExclusiveLock on the cron job record, but the latter
+ # is locked by the cron system for the duration of the job!
+ # FIXME: the subscription module should be reviewed to simplify the scheduling process
+ # and to use a unique cron job for all subscriptions, so that it never needs to
+ # be updated during its execution.
+ cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" % (self._table, '%s_cron_id_fkey' % self._table))
+
def set_process(self, cr, uid, ids, context=None):
for row in self.read(cr, uid, ids, context=context):
mapping = {'name':'name','interval_number':'interval_number','interval_type':'interval_type','exec_init':'numbercall','date_init':'nextcall'}