diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-06-11 19:20:38 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-06-11 19:20:38 +0200 |
| commit | b6496f8b38f504dd3c396e552c82c8801343947d (patch) | |
| tree | 4b44060c645c0086dd50874b7f08d5369b79ea11 | |
| parent | c0618d72d0d452084c9e754cac7380ce90dad748 (diff) | |
| parent | 9032617120138848c63b3cfa5d1913c5e5ad76db (diff) | |
[MERGE] forward port branch 10.0 up to 90326171201
29 files changed, 134 insertions, 29 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ef24d91ae2e..f440025be26 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1262,6 +1262,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'): @@ -1280,8 +1287,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): @@ -1330,8 +1339,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 537a945c6df..c60f1ca1c96 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 cfc51a41e6c..cbd845e6f8e 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -240,14 +240,16 @@ <span><t t-esc="dashboard.account_balance"/></span> </div> </div> - <div class="row" name="latest_statement" t-if="dashboard.last_balance != dashboard.account_balance"> - <div class="col-xs-6"> - <span title="Latest Statement">Latest Statement</span> - </div> - <div class="col-xs-6 text-right"> - <span><t t-esc="dashboard.last_balance"/></span> + <t t-if="dashboard.last_balance != dashboard.account_balance"> + <div class="row" name="latest_statement"> + <div class="col-xs-6"> + <span title="Latest Statement">Latest Statement</span> + </div> + <div class="col-xs-6 text-right"> + <span><t t-esc="dashboard.last_balance"/></span> + </div> </div> - </div> + </t> </div> </t> <t t-name="JournalBodySalePurchase"> 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/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 @@ <label for="duration"/> <div> <button style="pointer-events: none;" class="oe_inline label label-default"> - <field name="duration" widget="mrp_time_counter"/> + <field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/> </button> </div> </group> 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 1efa9c90c79..6f683d23638 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -31,8 +31,8 @@ class PosSession(models.Model): if order.state not in ('paid'): raise UserError(_("You cannot confirm all orders of this session, because they don't have the 'paid' status")) 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 451f391c7da..6fcfbd3457e 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 81192b03e0f..1db875927b7 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -35,12 +35,16 @@ var EventRegistrationForm = Widget.extend({ return; } 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/<model("im_livechat.channel"):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 612a9b6e138..8a165ef2527 100644 --- a/addons/website_project_issue/models/project_issue.py +++ b/addons/website_project_issue/models/project_issue.py @@ -36,7 +36,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 91ea8a199cd..8cba6d0b604 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -615,7 +615,7 @@ <input type="hidden" t-if="len(product.product_variant_ids) == 1" name="product_id" t-att-value="product.product_variant_id.id"/> <t t-if="len(product.product_variant_ids) > 1"> <label label-default="label-default" class="radio" t-foreach="product.product_variant_ids" t-as="variant_id"> - <input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else ''" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/> + <input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else None" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/> <span t-esc="variant_id.name_get()[0][1]"/> <t t-set="diff_price" t-value="variant_id.price - variant_id.product_tmpl_id.price"/> <span class="badge" t-if="diff_price != 0"> diff --git a/doc/cla/corporate/wt-io-it.md b/doc/cla/corporate/wt-io-it.md index 491d580c792..c9bbebc2445 100644 --- a/doc/cla/corporate/wt-io-it.md +++ b/doc/cla/corporate/wt-io-it.md @@ -13,3 +13,4 @@ Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wt-io-it List of contributors: Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wtaferner +Andreas Perhab andreas.perhab@wt-io-it.at https://github.com/ap-wtioit 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 a613e0c8e39..424cb9040e1 100644 --- a/doc/setup/enterprise.rst +++ b/doc/setup/enterprise.rst @@ -45,7 +45,7 @@ On Linux, using an installer .. code-block:: console - $ python /usr/bin/odoo.py -d <database_name> -i web_enterprise --stop-after-init + $ python /usr/bin/odoo-bin -d <database_name> -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 a6c6b201bdf..85ec8b8d0b1 100644 --- a/odoo/addons/base/ir/ir_mail_server.py +++ b/odoo/addons/base/ir/ir_mail_server.py @@ -20,6 +20,8 @@ from odoo.tools import html2text, ustr _logger = logging.getLogger(__name__) _test_logger = logging.getLogger('odoo.tests') +SMTP_TIMEOUT = 60 + class MailDeliveryException(except_orm): """Specific exception subclass for mail delivery errors""" @@ -240,9 +242,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 fd8863701aa..f06c82dc912 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -780,6 +780,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, xml_declaration=True, encoding="utf-8") + 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() view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) def get_application_groups(self, domain): diff --git a/odoo/addons/base/security/base_security.xml b/odoo/addons/base/security/base_security.xml index b02ebfedc18..1242e7e436d 100644 --- a/odoo/addons/base/security/base_security.xml +++ b/odoo/addons/base/security/base_security.xml @@ -142,7 +142,7 @@ <field name="name">res.partner.rule.private.employee</field> <field name="model_id" ref="base.model_res_partner"/> <field name="domain_force"> - [('type', '!=', 'private')] + ['|', ('type', '!=', 'private'), ('type', '=', False)] </field> <field name="groups" eval="[ (4, ref('base.group_user')), diff --git a/odoo/models.py b/odoo/models.py index e0540cd4056..108df85a318 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4376,7 +4376,7 @@ class BaseModel(object): vals = self.copy_data(default)[0] # To avoid to create a translation in the lang of the user, copy_translation will do it new = self.with_context(lang=None).create(vals) - self.copy_translations(new) + self.with_context(from_copy_translation=True).copy_translations(new) return new @api.multi diff --git a/odoo/service/server.py b/odoo/service/server.py index 6e4284a1a85..be3de54d529 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -356,7 +356,10 @@ class GeventServer(CommonServer): def start(self): import gevent - from gevent.wsgi import WSGIServer + try: + from gevent.pywsgi import WSGIServer + except ImportError: + from gevent.wsgi import WSGIServer if os.name == 'posix': @@ -70,6 +70,7 @@ def py2exe_options(): 'dist_dir': 'dist', 'packages': [ 'asynchat', 'asyncore', + 'BeautifulSoup', 'commands', 'dateutil', 'decimal', |
