diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 7699ac87723..997c5cf8f67 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1507,6 +1507,13 @@ class AccountInvoiceLine(models.Model):
return accounts['income']
return accounts['expense']
+ def _set_currency(self):
+ company = self.invoice_id.company_id
+ currency = self.invoice_id.currency_id
+ if company and currency:
+ if company.currency_id != currency:
+ self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
+
def _set_taxes(self):
""" Used in on_change to set taxes and price."""
if self.invoice_id.type in ('out_invoice', 'out_refund'):
@@ -1525,8 +1532,10 @@ class AccountInvoiceLine(models.Model):
prec = self.env['decimal.precision'].precision_get('Product Price')
if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0:
self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes)
+ self._set_currency()
else:
self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes)
+ self._set_currency()
@api.onchange('product_id')
def _onchange_product_id(self):
@@ -1575,8 +1584,6 @@ class AccountInvoiceLine(models.Model):
domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
if company and currency:
- if company.currency_id != currency:
- self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
if self.uom_id and self.uom_id.id != product.uom_id.id:
self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id)
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index fe1606a087b..c59902783a6 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -14,7 +14,7 @@ class ResConfigSettings(models.TransientModel):
'account.journal',
related='company_id.currency_exchange_journal_id',
string="Exchange Gain or Loss Journal",
- domain=[('type', '=', 'general')],
+ domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
help='The accounting journal where automatic exchange differences will be registered')
has_chart_of_accounts = fields.Boolean(compute='_compute_has_chart_of_accounts', string='Company has a chart of accounts')
chart_template_id = fields.Many2one('account.chart.template', string='Template',
diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml
index 5db552205ef..2f76551912a 100644
--- a/addons/account/views/account_journal_dashboard_view.xml
+++ b/addons/account/views/account_journal_dashboard_view.xml
@@ -248,14 +248,16 @@
-
-
-
Latest Statement
+
+
+
+ Latest Statement
+
+
+
+
-
-
-
-
+
diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py
index 8118d447592..8c8b6626b01 100644
--- a/addons/account_bank_statement_import/account_bank_statement_import.py
+++ b/addons/account_bank_statement_import/account_bank_statement_import.py
@@ -50,7 +50,11 @@ class AccountBankStatementImport(models.TransientModel):
# Create the bank statements
statement_ids, notifications = self._create_bank_statements(stmts_vals)
# Now that the import worked out, set it as the bank_statements_source of the journal
- journal.bank_statements_source = 'file_import'
+ if journal.bank_statements_source != 'file_import':
+ # Use sudo() because only 'account.group_account_manager'
+ # has write access on 'account.journal', but 'account.group_account_user'
+ # must be able to import bank statement files
+ journal.sudo().bank_statements_source = 'file_import'
# Finally dispatch to reconciliation interface
action = self.env.ref('account.action_bank_reconcile_bank_statements')
return {
diff --git a/addons/event/models/res_partner.py b/addons/event/models/res_partner.py
index d534f3749f2..727de940fa1 100644
--- a/addons/event/models/res_partner.py
+++ b/addons/event/models/res_partner.py
@@ -10,7 +10,7 @@ class ResPartner(models.Model):
def _compute_event_count(self):
for partner in self:
- partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
+ partner.event_count = self.env['event.event'].sudo().search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
@api.multi
def action_event_view(self):
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index 8e5ce0ad6c8..0bcde86522f 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -80,6 +80,8 @@ class MrpBom(models.Model):
def onchange_product_tmpl_id(self):
if self.product_tmpl_id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
+ if self.product_id.product_tmpl_id != self.product_tmpl_id:
+ self.product_id = False
@api.onchange('routing_id')
def onchange_routing_id(self):
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index f9e0bd5eadc..8bbe08d4570 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -358,6 +358,7 @@ class MrpWorkorder(models.Model):
move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
if move_line:
move_line.product_uom_qty += self.qty_producing
+ move_line.qty_done += self.qty_producing
else:
move_line.create({'move_id': production_move.id,
'product_id': production_move.product_id.id,
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index a34b4df2ac8..8bcac2fe7b2 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -190,7 +190,7 @@
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index d9f38720fd4..f219847c8d7 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -38,8 +38,8 @@ class PosSession(models.Model):
paid=order.amount_paid,
))
order.action_pos_order_done()
- orders = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'])
- orders.sudo()._reconcile_payments()
+ orders_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id)
+ orders_to_reconcile.sudo()._reconcile_payments()
config_id = fields.Many2one(
'pos.config', string='Point of Sale',
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index 0c8206c86f6..50c623a19e2 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -140,7 +140,7 @@ class PortalWizardUser(models.TransientModel):
if wizard_user.partner_id.company_id:
company_id = wizard_user.partner_id.company_id.id
else:
- company_id = self.env['res.company']._company_default_get('res.users')
+ company_id = self.env['res.company']._company_default_get('res.users').id
user_portal = wizard_user.sudo().with_context(company_id=company_id)._create_user()
else:
user_portal = user
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 77b17e5d17c..fe04d0939dc 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -222,6 +222,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
self.code = supplier_info.product_code or self.default_code
+ break
else:
self.code = self.default_code
@@ -230,6 +231,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
product_name = supplier_info.product_name or self.default_code
+ break
self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name)
else:
self.partner_ref = self.name_get()[0][1]
diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py
index cb24f04241d..c07bca51701 100644
--- a/addons/product/tests/__init__.py
+++ b/addons/product/tests/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import test_variants, test_uom, test_pricelist, test_product_pricelist
+from . import test_seller, test_variants, test_uom, test_pricelist, test_product_pricelist
diff --git a/addons/product/tests/test_seller.py b/addons/product/tests/test_seller.py
new file mode 100644
index 00000000000..36693317c34
--- /dev/null
+++ b/addons/product/tests/test_seller.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestSeller(TransactionCase):
+
+ def setUp(self):
+ super(TestSeller, self).setUp()
+ self.product_service = self.env.ref('product.product_product_2')
+ self.product_service.default_code = 'DEFCODE'
+ self.asustec = self.env.ref('base.res_partner_1')
+ self.camptocamp = self.env.ref('base.res_partner_12')
+
+ def test_10_sellers(self):
+ self.product_service.write({'seller_ids': [
+ (0, 0, {'name': self.asustec.id, 'product_code': 'ASUCODE'}),
+ (0, 0, {'name': self.camptocamp.id, 'product_code': 'C2CCODE'}),
+ ]})
+
+ default_code = self.product_service.code
+ self.assertEqual("DEFCODE", default_code, "Default code not used in product name")
+
+ context_code = self.product_service\
+ .with_context(partner_id=self.camptocamp.id)\
+ .code
+ self.assertEqual('C2CCODE', context_code, "Partner's code not used in product name with context set")
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 2798faaf12a..e6cac2bd622 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -753,7 +753,7 @@ class Task(models.Model):
groups = [new_group] + groups
for group_name, group_method, group_data in groups:
- if group_name in ['customer', 'portal']:
+ if group_name == 'customer':
continue
group_data['has_button_access'] = True
diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js
index ebe38118559..53a304c707e 100644
--- a/addons/website_event/static/src/js/website_event.js
+++ b/addons/website_event/static/src/js/website_event.js
@@ -59,12 +59,16 @@ var EventRegistrationForm = Widget.extend({
$button.attr('disabled', true);
return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) {
var $modal = $(modal);
+ $modal.modal({backdrop: 'static', keyboard: false});
$modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19
$modal.insertAfter($form).modal();
$modal.on('click', '.js_goto_event', function () {
$modal.modal('hide');
$button.prop('disabled', false);
});
+ $modal.on('click', '.close', function () {
+ $button.prop('disabled', false);
+ });
});
}
},
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 1b9b7a8ce6d..26c8886f183 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -422,6 +422,11 @@ class Post(models.Model):
raise KarmaError('User karma not sufficient to post an image or link.')
return content
+ @api.constrains('parent_id')
+ def _check_parent_id(self):
+ if not self._check_recursion():
+ raise ValidationError(_('You cannot create recursive forum posts.'))
+
@api.model
def create(self, vals):
if 'content' in vals and vals.get('forum_id'):
diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py
index 753e4eef1ae..31af89c2e63 100644
--- a/addons/website_livechat/controllers/main.py
+++ b/addons/website_livechat/controllers/main.py
@@ -20,13 +20,17 @@ class WebsiteLivechat(http.Controller):
@http.route('/livechat/channel/', type='http', auth='public', website=True)
def channel_rating(self, channel, **kw):
# get the last 100 ratings and the repartition per grade
- ratings = request.env['rating.rating'].search([('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids)], order='create_date desc', limit=100)
- repartition = channel.sudo().channel_ids.rating_get_grades()
+ domain = [
+ ('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids),
+ ('consumed', '=', True), ('rating', '>=', 1),
+ ]
+ ratings = request.env['rating.rating'].search(domain, order='create_date desc', limit=100)
+ repartition = channel.sudo().channel_ids.rating_get_grades(domain=domain)
# compute percentage
percentage = dict.fromkeys(['great', 'okay', 'bad'], 0)
for grade in repartition:
- percentage[grade] = repartition[grade] * 100 / sum(repartition.values()) if sum(repartition.values()) else 0
+ percentage[grade] = round(repartition[grade] * 100.0 / sum(repartition.values()), 1) if sum(repartition.values()) else 0
# the value dict to render the template
values = {
diff --git a/doc/cla/corporate/shine-it.md b/doc/cla/corporate/shine-it.md
new file mode 100644
index 00000000000..975f9d885f4
--- /dev/null
+++ b/doc/cla/corporate/shine-it.md
@@ -0,0 +1,17 @@
+China, 2018-04-28
+
+Shine IT agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Tony Gu tony@openerp.cn https://github.com/digitalsatori
+
+List of contributors:
+Tony Gu tony@openerp.cn https://github.com/digitalsatori
+Joshua Jan joshua@openerp.cn https://github.com/joshuajan
+Xiao Guo xerxes@openerp.cn https://github.com/xerxesnoPT
+
diff --git a/doc/cla/individual/Zelwak.md b/doc/cla/individual/Zelwak.md
new file mode 100644
index 00000000000..40c8f303d42
--- /dev/null
+++ b/doc/cla/individual/Zelwak.md
@@ -0,0 +1,11 @@
+
+
+France, 2018-06-07
+
+I here by agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Flavien CHÊNE me@flavienchene.fr https://github.com/Zelwak
diff --git a/doc/cla/individual/mduvergey.md b/doc/cla/individual/mduvergey.md
new file mode 100644
index 00000000000..3b9422711ec
--- /dev/null
+++ b/doc/cla/individual/mduvergey.md
@@ -0,0 +1,11 @@
+France, 2018-05-24
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Martin Duvergey martin.duvergey@gpsante.fr https://github.com/mduvergey
diff --git a/doc/cla/individual/schout-it.md b/doc/cla/individual/schout-it.md
new file mode 100644
index 00000000000..35451db875d
--- /dev/null
+++ b/doc/cla/individual/schout-it.md
@@ -0,0 +1,11 @@
+Belgium, 2018-05-17
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Rod Schouteden rod@schout-it.be https://github.com/schout-it
diff --git a/doc/setup/enterprise.rst b/doc/setup/enterprise.rst
index 49ba8af4c84..66f8fe82671 100644
--- a/doc/setup/enterprise.rst
+++ b/doc/setup/enterprise.rst
@@ -46,7 +46,7 @@ On Linux, using an installer
.. code-block:: console
- $ python3 /usr/bin/odoo.py -d -i web_enterprise --stop-after-init
+ $ python3 /usr/bin/odoo-bin -d -i web_enterprise --stop-after-init
* You should be able to connect to your Odoo Enterprise instance using your usual mean of identification.
You can then link your database with your Odoo Enterprise Subscription by entering the code you received
diff --git a/odoo/addons/base/ir/ir_mail_server.py b/odoo/addons/base/ir/ir_mail_server.py
index 96edff27263..454e2dc0780 100644
--- a/odoo/addons/base/ir/ir_mail_server.py
+++ b/odoo/addons/base/ir/ir_mail_server.py
@@ -22,6 +22,8 @@ from odoo.tools import ustr, pycompat
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
+SMTP_TIMEOUT = 60
+
class MailDeliveryException(except_orm):
"""Specific exception subclass for mail delivery errors"""
@@ -225,9 +227,9 @@ class IrMailServer(models.Model):
"You could use STARTTLS instead. "
"If SSL is needed, an upgrade to Python 2.6 on the server-side "
"should do the trick."))
- connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
+ connection = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
else:
- connection = smtplib.SMTP(smtp_server, smtp_port)
+ connection = smtplib.SMTP(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
connection.set_debuglevel(smtp_debug)
if smtp_encryption == 'starttls':
# starttls() will perform ehlo() if needed first
diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py
index 8db66e282ec..201f311c75d 100644
--- a/odoo/addons/base/res/res_users.py
+++ b/odoo/addons/base/res/res_users.py
@@ -790,6 +790,11 @@ class GroupsView(models.Model):
xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")
+ if not view.check_access_rights('write', raise_exception=False):
+ # erp manager has the rights to update groups/users but not
+ # to modify ir.ui.view
+ if self.env.user.has_group('base.group_erp_manager'):
+ view = view.sudo()
new_context = dict(view._context)
new_context.pop('install_mode_data', None) # don't set arch_fs for this computed view
diff --git a/odoo/addons/base/security/base_security.xml b/odoo/addons/base/security/base_security.xml
index cce42d7bf2e..26190f53ff6 100644
--- a/odoo/addons/base/security/base_security.xml
+++ b/odoo/addons/base/security/base_security.xml
@@ -152,7 +152,7 @@
res.partner.rule.private.employee
- [('type', '!=', 'private')]
+ ['|', ('type', '!=', 'private'), ('type', '=', False)]