diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index a942f550c07..378d2bf4f0e 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1270,6 +1270,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'):
@@ -1288,8 +1295,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):
@@ -1338,8 +1347,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.py b/addons/account/models/res_config.py
index c0ed2152479..791c6e60aac 100644
--- a/addons/account/models/res_config.py
+++ b/addons/account/models/res_config.py
@@ -18,7 +18,7 @@ class AccountConfigSettings(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 ff126e0ae67..8fa1e2938db 100644
--- a/addons/account/views/account_journal_dashboard_view.xml
+++ b/addons/account/views/account_journal_dashboard_view.xml
@@ -240,14 +240,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 9056b7af916..a25182d079f 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/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py
index b8b46f2a7dc..b6044efc990 100644
--- a/addons/crm/wizard/base_partner_merge.py
+++ b/addons/crm/wizard/base_partner_merge.py
@@ -260,7 +260,7 @@ class MergePartnerAutomatic(models.TransientModel):
"""
_logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)
- model_fields = dst_partner._fields
+ model_fields = dst_partner.fields_get().keys()
def write_serializer(item):
if isinstance(item, models.BaseModel):
@@ -269,7 +269,8 @@ class MergePartnerAutomatic(models.TransientModel):
return item
# get all fields that are not computed or x2many
values = dict()
- for column, field in model_fields.iteritems():
+ for column in model_fields:
+ field = dst_partner._fields[column]
if field.type not in ('many2many', 'one2many') and field.compute is None:
for item in itertools.chain(src_partners, [dst_partner]):
if item[column]:
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 e61ffdadea9..a1bf245909d 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -81,6 +81,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 6f640d6c695..410e0f2af7c 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -318,6 +318,7 @@ class MrpWorkorder(models.Model):
move_lot = production_move.move_lot_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
if move_lot:
move_lot.quantity += self.qty_producing
+ move_lot.quantity_done += self.qty_producing
else:
move_lot.create({'move_id': production_move.id,
'lot_id': self.final_lot_id.id,
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index 5e89bed4fa0..a786ddd7465 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -191,7 +191,7 @@
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index a5dec077e7d..c1ddb41351b 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -79,6 +79,7 @@ odoo.define('payment_stripe.stripe', function(require) {
})
handler.open({
name: $("input[name='merchant']").val(),
+ email: $("input[name='email']").val(),
description: $("input[name='invoice_num']").val(),
currency: currency,
amount: _.contains(int_currencies, currency) ? amount : amount * 100,
@@ -94,6 +95,7 @@ odoo.define('payment_stripe.stripe', function(require) {
$form.html(data);
handler.open({
name: $("input[name='merchant']").val(),
+ email: $("input[name='email']").val(),
description: $("input[name='invoice_num']").val(),
currency: currency,
amount: _.contains(int_currencies, currency) ? amount : amount * 100,
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 535db66e093..49ced5310eb 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 09ded942500..ed37bd6fbc6 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -245,6 +245,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
@@ -253,6 +254,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
else:
product_name = self.name
self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name)
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/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js
index 74bfb9b980c..899aa6ea4d1 100644
--- a/addons/website_event/static/src/js/website_event.js
+++ b/addons/website_event/static/src/js/website_event.js
@@ -42,12 +42,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 7864b95b72f..231adae16d0 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -427,6 +427,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/addons/website_project_issue/models/project_issue.py b/addons/website_project_issue/models/project_issue.py
index 7b4fbb0b246..7ccf273e858 100644
--- a/addons/website_project_issue/models/project_issue.py
+++ b/addons/website_project_issue/models/project_issue.py
@@ -49,7 +49,7 @@ class Issue(models.Model):
groups = super(Issue, self)._notification_recipients(message, 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_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index bab625d08f0..55ed4fae05b 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -617,7 +617,7 @@