diff options
| author | Olivier Dony <odo@openerp.com> | 2017-05-16 17:09:17 +0200 |
|---|---|---|
| committer | Olivier Dony <odo@openerp.com> | 2017-05-16 17:09:45 +0200 |
| commit | 5afe894f440eff3b58134d14fa7c9b43b19fe88b (patch) | |
| tree | 2b89e105c3389187dc9f1707e88a0acdc531a1a1 /addons | |
| parent | 8dac9cfc93cb4196007bd08dd8b1304cd522e615 (diff) | |
| parent | 878fbc75ff9ebade92f43748500e64350367a824 (diff) | |
[MERGE] Forward-port saas-15 up to 878fbc75ff9ebade92f43748500e64350367a824
Diffstat (limited to 'addons')
41 files changed, 313 insertions, 140 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index fc2a4bd0c36..71ed7b11a7e 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -426,7 +426,7 @@ class AccountBankStatementLine(models.Model): payment_to_unreconcile |= line.payment_id if st_line.move_name and line.payment_id.payment_reference == st_line.move_name: #there can be several moves linked to a statement line but maximum one created by the line itself - aml_to_cancel |= st_line.journal_entry_ids + aml_to_cancel |= line payment_to_cancel |= line.payment_id aml_to_unbind = aml_to_unbind - aml_to_cancel @@ -887,6 +887,8 @@ class AccountBankStatementLine(models.Model): if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types): # Transform the value in the format required for One2many and Many2many fields aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids']) + if any(line.journal_entry_ids for line in self): + raise UserError(_('A selected statement line was already reconciled with an account move.')) # Fully reconciled moves are just linked to the bank statement total = self.amount diff --git a/addons/account/models/company.py b/addons/account/models/company.py index b15cb4afe0b..44eee5432ae 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -79,8 +79,7 @@ Best Regards,''') account.write({'code': account.code.rstrip('0').ljust(digits, '0')}) @api.multi - def write(self, values): - #restrict the closing of FY if there are still unposted entries + def _validate_fiscalyear_lock(self, values): if values.get('fiscalyear_lock_date'): nb_draft_entries = self.env['account.move'].search([ ('company_id', 'in', [c.id for c in self]), @@ -88,6 +87,12 @@ Best Regards,''') ('date', '<=', values['fiscalyear_lock_date'])]) if nb_draft_entries: raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.')) + + @api.multi + def write(self, values): + #restrict the closing of FY if there are still unposted entries + self._validate_fiscalyear_lock(values) + # Reflect the change on accounts for company in self: digits = values.get('accounts_code_digits') or company.accounts_code_digits diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 042455837ce..b028301af6f 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -108,7 +108,7 @@ class AccountInvoiceReport(models.Model): 1 AS nbr, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, - SUM ((invoice_type.sign * ail.quantity) / (u.factor * u2.factor)) AS product_qty, + SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty, SUM(ail.price_subtotal_signed) AS price_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 6c1b0facad3..dcaba89bc61 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -57,6 +57,9 @@ class AuthSignupHome(Home): else: login = qcontext.get('login') assert login, "No login provided." + _logger.info( + "Password reset attempt for <%s> by user <%s> from %s", + login, request.env.user.login, request.httprequest.remote_addr) request.env['res.users'].sudo().reset_password(login) qcontext['message'] = _("An email has been sent with credentials to reset your password") except SignupError: diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index d99961076be..05d52596c85 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging from ast import literal_eval @@ -11,6 +12,7 @@ from odoo.tools.misc import ustr from odoo.addons.base.ir.ir_mail_server import MailDeliveryException from odoo.addons.auth_signup.models.res_partner import SignupError, now +_logger = logging.getLogger(__name__) class ResUsers(models.Model): _inherit = 'res.users' @@ -134,6 +136,7 @@ class ResUsers(models.Model): if not user.email: raise UserError(_("Cannot send email: user %s has no email address.") % user.name) template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True) + _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email) @api.model def create(self, values): diff --git a/addons/base_automation/views/base_automation_view.xml b/addons/base_automation/views/base_automation_view.xml index fa8c4b5eab2..a81dd04d75b 100644 --- a/addons/base_automation/views/base_automation_view.xml +++ b/addons/base_automation/views/base_automation_view.xml @@ -15,6 +15,13 @@ attrs="{'invisible':[('menu_ir_values_id','=',False)]}" help="Remove the contextual action related to this server action"/> </xpath> + <div class="oe_title" position="before"> + <div class="oe_button_box" name="button_box"> + <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive"> + <field name="active" widget="boolean_button"/> + </button> + </div> + </div> <xpath expr="//group[@name='action_wrapper']" position="inside"> <group> <field name="trigger"/> diff --git a/addons/google_spreadsheet/views/base_config_settings_views.xml b/addons/google_spreadsheet/views/base_config_settings_views.xml index 14bda91e373..6afd8ddacda 100644 --- a/addons/google_spreadsheet/views/base_config_settings_views.xml +++ b/addons/google_spreadsheet/views/base_config_settings_views.xml @@ -8,13 +8,10 @@ <field name="arch" type="xml"> <xpath expr="//div[@id='msg_module_google_spreadsheet']" position="replace"> <div class="content-group" attrs="{'invisible': [('module_google_spreadsheet','=',False)]}"> - <div class="row mt16"> - <label for="google_drive_authorization_code" class="col-md-3 o_light_label"/> - <field name="google_drive_authorization_code"/> - </div> - <a type="button" class="btn-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/> - <field name="google_drive_uri_copy" widget="url" text="Generate Code"/> - </a> + <div class="content-group mt16"> + Please use the settings of Google Drive + on the left or above. + </div> </div> </xpath> </field> diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index c2f0e8cb3c2..20dc2939d00 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -166,7 +166,7 @@ class ImLivechatChannel(models.Model): operator_partner_id = user.partner_id.id # partner to add to the mail.channel channel_partner_to_add = [(4, operator_partner_id)] - if self.env.uid: # if the user if logged (portal user), he can be identify + if self.env.user and self.env.user.active: # valid session user (not public) channel_partner_to_add.append((4, self.env.user.partner_id.id)) # create the session, and add the link with the given channel mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({ diff --git a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml index 883a7c75f1e..85ae97c2248 100644 --- a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml +++ b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml @@ -48,7 +48,7 @@ </td> <td> <span t-esc="o.employee_id.id or '_'"/><br/> - <span t-esc="o.employee_id.ssnid or '_'"/><br/> + <span t-esc="o.employee_id.identification_id or '_'"/><br/> <span t-esc="o.contract_id.job_id.name or '_'"/><br/> <span t-esc="o.contract_id.qualif or '_'"/><br/> <span t-esc="o.contract_id.niveau or '_'"/><br/> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index cc3519ef737..4c3bab77de1 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -507,7 +507,7 @@ class MassMailing(models.Model): return super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby) def update_opt_out(self, email, res_ids, value): - model = self.env[self.mailing_model_real] + model = self.env[self.mailing_model_real].with_context(active_test=False) if 'opt_out' in model._fields: email_fname = 'email_from' if 'email' in model._fields: diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js index 4475d001e5d..5d475808589 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js @@ -273,9 +273,13 @@ snippets_editor.Class.include({ function editable_area_is_empty($layout) { $layout = $layout || $editable_area.find(".o_layout"); var $mail_wrapper = $layout.children(".o_mail_wrapper"); + var $mail_wrapper_content = $mail_wrapper.find('.o_mail_wrapper_td'); + if (!$mail_wrapper_content.length) { // compatibility + $mail_wrapper_content = $mail_wrapper; + } return ( $editable_area.html().trim() === "" - || ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper.length > 0 && $mail_wrapper.html().trim() === "")) + || ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper_content.length > 0 && $mail_wrapper_content.html().trim() === "")) ); } @@ -325,7 +329,15 @@ snippets_editor.Class.include({ switch_images(theme_params, $editable_area); var $old_layout = $editable_area.find(".o_layout"); - var $new_wrapper = $("<div/>", {"class": "o_mail_wrapper oe_structure"}); + // This wrapper structure is the only way to have a responsive and + // centered fixed-width content column on all mail clients + var $new_wrapper = $('<table/>', {class: 'o_mail_wrapper'}); + var $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'}); + $new_wrapper.append($('<tr/>').append( + $("<td/>", {class: 'o_mail_no_resize'}), + $new_wrapper_content, + $("<td/>", {class: 'o_mail_no_resize'}) + )); var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper); var $contents; @@ -338,11 +350,11 @@ snippets_editor.Class.include({ } $editable_area.empty().append($new_layout); - $new_wrapper.append($contents); + $new_wrapper_content.append($contents); $old_layout.remove(); if (first_choice) { - self.add_default_snippet_text_classes($new_wrapper); + self.add_default_snippet_text_classes($new_wrapper_content); } self.show_blocks(); } diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less index 98f86421000..74f71191db2 100644 --- a/addons/mass_mailing/static/src/less/themes/theme_default.less +++ b/addons/mass_mailing/static/src/less/themes/theme_default.less @@ -49,27 +49,36 @@ // ===== Layout ===== .o_layout { - font-family: @o_mm_def_font; - color: lighten(@o_mm_def_text_color, 20%); overflow: hidden; - width: 100%; + width: 94%; min-height: 100%; background-color: @o_mm_def_color_alpha; - padding: 24px 0; + padding: 24px 3%; + + // Forces on <p/> elements as several mail clients does not correctly + // inherit font properties + &, p { + font-family: @o_mm_def_font; + color: lighten(@o_mm_def_text_color, 20%); + } > .o_mail_wrapper { width: 100%; - max-width: @o_mm_def_body_width; - margin: auto; - border: 1px solid darken(@o_mm_def_color_alpha, 2%); - background-color: @o_mm_def_color_beta; + border-collapse: separate; // Allow the first and last td to have the + // same width + + .o_mail_wrapper_td { + width: @o_mm_def_body_width; + border: 1px solid darken(@o_mm_def_color_alpha, 2%); + background-color: @o_mm_def_color_beta; + } } } // ===== Snippets (general) ===== .o_mail_snippet_general { width: 100%; - max-width: @o_mm_def_body_width; + max-width: @o_mm_def_body_width; // should not be necessary thanks to mail wrapper margin: 0 auto; h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { @@ -157,7 +166,11 @@ } .o_mail_block_two_cols, .o_mail_block_three_cols { - font-size: small; + // Forces on <p/> elements as several mail clients does not correctly + // inherit font properties + &, p { + font-size: small; + } .o_mail_col_container { padding: 10px 20px; @@ -169,7 +182,11 @@ } .o_mail_block_image_text, .o_mail_block_text_image { - font-size: small; + // Forces on <p/> elements as several mail clients does not correctly + // inherit font properties + &, p { + font-size: small; + } } .o_mail_block_event { diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 5fd40f455c7..ac799bd0538 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -499,11 +499,14 @@ class MrpProduction(models.Model): @api.multi def post_inventory(self): for order in self: + moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves_to_do.action_done() + moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do order._cal_price(moves_to_do) moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel')) moves_to_finish.action_done() + for move in moves_to_finish: #Group quants by lots lot_quants = {} diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index bb98284f9f2..b2fd5ea0ab6 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -507,7 +507,7 @@ class PaymentTransaction(models.Model): tx.write({'reference': str(tx.id)}) # Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff - if tx.callback_model_id and tx.callback_res_id and tx.callback_method: + if tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method: tx.write({'callback_hash': tx._generate_callback_hash()}) return tx @@ -547,7 +547,9 @@ class PaymentTransaction(models.Model): def _generate_callback_hash(self): self.ensure_one() secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') - token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id, self.callback_method) + token = '%s%s%s' % (self.callback_model_id.model, + self.callback_res_id, + self.sudo().callback_method) return hmac.new(str(secret), token, hashlib.sha256).hexdigest() # -------------------------------------------------- @@ -635,7 +637,7 @@ class PaymentTransaction(models.Model): @api.multi def execute_callback(self): res = None - for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.callback_method): + for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method): valid_token = transaction._generate_callback_hash() if not consteq(ustr(valid_token), transaction.callback_hash): _logger.warning("Invalid callback signature for transaction %d" % (transaction.id)) @@ -643,7 +645,7 @@ class PaymentTransaction(models.Model): record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists() if record: - res = getattr(record, transaction.callback_method)(transaction) + res = getattr(record, transaction.sudo().callback_method)(transaction) else: _logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id)) return res diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ed5335c1c46..2bb4fb23dd9 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -677,7 +677,7 @@ class PosOrder(models.Model): qty = 0 qty_done = 0 pack_lots = [] - pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)]) + pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)]) pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots] if pack_lot_names and lots_necessary: diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 04c59ac13fb..71e4ca2c1c8 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -753,7 +753,7 @@ <t t-if='simple'> <line> <left><t t-esc='line.product_name_wrapped[0]' /></left> - <right><value><t t-esc='line.price_display' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'>><t t-esc='line.price_display' /></value></right> </line> <t t-call="XmlReceiptWrappedProductNameLines"/> </t> @@ -765,19 +765,19 @@ </t> <line indent='1'> <left> - <value value-decimals='3' value-autoint='on'> + <value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'> <t t-esc='line.quantity' /> </value> <t t-if='line.unit_name !== "Unit(s)"'> <t t-esc='line.unit_name' /> </t> x - <value value-decimals='2'> + <value t-att-value-decimals='pos.dp["Product Price"]'> <t t-esc='line.price' /> </value> </left> <right> - <value><t t-esc='line.price_display' /></value> + <value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value> </right> </line> </t> @@ -789,11 +789,11 @@ <t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) <= 0.000001' /> <t t-if='!taxincluded'> <line><right>--------</right></line> - <line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line> + <line><left>Subtotal</left><right><value t-att-value-decimals='pos.currency.decimals'><t t-esc="receipt.subtotal" /></value></right></line> <t t-foreach='receipt.tax_details' t-as='tax'> <line> <left><t t-esc='tax.name' /></left> - <right><value><t t-esc='tax.amount' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right> </line> </t> </t> @@ -803,7 +803,7 @@ <line><right>--------</right></line> <line class='total' size='double-height'> <left><pre> TOTAL</pre></left> - <right><value><t t-esc='receipt.total_with_tax' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_with_tax' /></value></right> </line> <br/><br/> @@ -812,14 +812,14 @@ <t t-foreach='paymentlines' t-as='line'> <line> <left><t t-esc='line.name' /></left> - <right><value><t t-esc='line.get_amount()'/></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.get_amount()'/></value></right> </line> </t> <br/> <line size='double-height'> <left><pre> CHANGE</pre></left> - <right><value><t t-esc='receipt.change' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.change' /></value></right> </line> <br/> @@ -828,19 +828,19 @@ <t t-if='receipt.total_discount'> <line> <left>Discounts</left> - <right><value><t t-esc='receipt.total_discount'/></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_discount'/></value></right> </line> </t> <t t-if='taxincluded'> <t t-foreach='receipt.tax_details' t-as='tax'> <line> <left><t t-esc='tax.name' /></left> - <right><value><t t-esc='tax.amount' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='tax.amount' /></value></right> </line> </t> <line> <left>Total Taxes</left> - <right><value><t t-esc='receipt.total_tax'/></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_tax'/></value></right> </line> </t> diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index 36748c5ea0c..ca392c4d895 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -17,7 +17,7 @@ class MercuryTransaction(models.Model): _name = 'pos_mercury.mercury_transaction' def _get_pos_session(self): - pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)]) + pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) if not pos_session: raise UserError("No POS session") @@ -34,6 +34,7 @@ class MercuryTransaction(models.Model): raise UserError("No Mercury configuration associated with the journal.") def _setup_request(self, data): + # todo: in master make the client include the pos.session id and use that pos_session = self._get_pos_session() config = pos_session.config_id diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 2494401ff00..67307794fbc 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -979,4 +979,11 @@ screens.define_action_button({ }, }); +return { + TableGuestsButton: TableGuestsButton, + TransferOrderButton:TransferOrderButton, + TableWidget: TableWidget, + FloorScreenWidget: FloorScreenWidget, +}; + }); diff --git a/addons/product/models/res_company.py b/addons/product/models/res_company.py index 3d8d0dcfc70..d94e8d2cda2 100644 --- a/addons/product/models/res_company.py +++ b/addons/product/models/res_company.py @@ -18,12 +18,13 @@ class ResCompany(models.Model): 'currency_id': new_company.currency_id.id, }) field = self.env['ir.model.fields']._get('res.partner', 'property_product_pricelist') - self.env['ir.property'].create({ + product_property = self.env['ir.property'].create({ 'name': 'property_product_pricelist', - 'company_id': new_company.id, 'value_reference': 'product.pricelist,%s' % pricelist.id, 'fields_id': field.id }) + # multi-company security rules prevents access + product_property.sudo().write({'company_id': new_company.id}) return new_company @api.multi diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 55abb28a638..3ff68a9052f 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -576,19 +576,23 @@ class Report(models.Model): """ writer = PdfFileWriter() streams = [] # We have to close the streams *after* PdfFilWriter's call to write() - for document in documents: - pdfreport = open(document, 'rb') - streams.append(pdfreport) - reader = PdfFileReader(pdfreport) - for page in range(0, reader.getNumPages()): - writer.addPage(reader.getPage(page)) - - merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.html', prefix='report.merged.tmp.') - with closing(os.fdopen(merged_file_fd, 'w')) as merged_file: - writer.write(merged_file) - - for stream in streams: - stream.close() + try: + for document in documents: + pdfreport = open(document, 'rb') + streams.append(pdfreport) + reader = PdfFileReader(pdfreport) + for page in range(0, reader.getNumPages()): + writer.addPage(reader.getPage(page)) + + merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.pdf', prefix='report.merged.tmp.') + with closing(os.fdopen(merged_file_fd, 'w')) as merged_file: + writer.write(merged_file) + finally: + for stream in streams: + try: + stream.close() + except Exception: + pass return merged_file_path diff --git a/addons/sale/report/sale_report.xml b/addons/sale/report/sale_report.xml index 3e4996dffe2..e0bad531aea 100644 --- a/addons/sale/report/sale_report.xml +++ b/addons/sale/report/sale_report.xml @@ -12,17 +12,5 @@ <record id="report_sale_order" model="ir.actions.report.xml"> <field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation'+'-'+(object.name) or 'Order'+'-'+(object.name))</field> </record> - - <report - string="Pro-Forma" - id="report_proforma_quotation" - model="sale.order" - report_type="qweb-pdf" - name="sale.report_saleproforma" - file="sale.report_saleorder" - /> - <record id="report_proforma_quotation" model="ir.actions.report.xml"> - <field name="print_report_name">'Pro-Forma'+'-'+(object.name)</field> - </record> </data> </odoo> diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml index b16c4d98192..71a6bbd144d 100644 --- a/addons/sale/report/sale_report_templates.xml +++ b/addons/sale/report/sale_report_templates.xml @@ -29,11 +29,11 @@ </div> <h2> - <t t-if="not proforma"> + <t t-if="not env.context.get('proforma', False)"> <span t-if="doc.state not in ['draft','sent']">Order # </span> <span t-if="doc.state in ['draft','sent']">Quotation # </span> </t> - <t t-if="proforma"> + <t t-if="env.context.get('proforma', False)"> <span>Pro-Forma # </span> </t> <span t-field="doc.name"/> diff --git a/addons/sale/security/sale_security.xml b/addons/sale/security/sale_security.xml index 32c1d36917e..0a269ab44bf 100644 --- a/addons/sale/security/sale_security.xml +++ b/addons/sale/security/sale_security.xml @@ -4,7 +4,6 @@ <record id="sale.group_sale_layout" model="res.groups"> <field name="name">Personalize sales order and invoice report</field> <field name="category_id" ref="base.module_category_hidden"/> - <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/> </record> <record id="group_delivery_invoice_address" model="res.groups"> diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 3e28cbb9575..886d80d19bd 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -82,10 +82,11 @@ class SaleAdvancePaymentInv(models.TransientModel): else: amount = self.amount name = _('Down Payment') - if order.fiscal_position_id and self.product_id.taxes_id: - tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids + taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) + if order.fiscal_position_id and taxes: + tax_ids = order.fiscal_position_id.map_tax(taxes).ids else: - tax_ids = self.product_id.taxes_id.ids + tax_ids = taxes.ids invoice = inv_obj.create({ 'name': order.client_order_ref or order.name, @@ -145,10 +146,11 @@ class SaleAdvancePaymentInv(models.TransientModel): raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) if self.product_id.type != 'service': raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) - if order.fiscal_position_id and self.product_id.taxes_id: - tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids + taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) + if order.fiscal_position_id and taxes: + tax_ids = order.fiscal_position_id.map_tax(taxes).ids else: - tax_ids = self.product_id.taxes_id.ids + tax_ids = taxes.ids so_line = sale_line_obj.create({ 'name': _('Advance: %s') % (time.strftime('%m %Y'),), 'price_unit': amount, diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py index 8aa4074c8bb..03aec004f03 100644 --- a/addons/sale_mrp/sale_mrp.py +++ b/addons/sale_mrp/sale_mrp.py @@ -42,6 +42,30 @@ class SaleOrderLine(models.Model): return 0.0 return super(SaleOrderLine, self)._get_delivered_qty() + @api.multi + def _get_bom_component_qty(self, bom): + bom_quantity = self.product_uom._compute_quantity(self.product_uom_qty, bom.product_uom_id) + boms, lines = bom.explode(self.product_id, bom_quantity) + components = {} + for line, line_data in lines: + product = line.product_id.id + uom = line.product_uom_id + qty = line.product_qty + if components.get(product, False): + if uom.id != components[product]['uom']: + from_uom = uom + to_uom = self.env['product.uom'].browse(components[product]['uom']) + qty = from_uom._compute_quantity(qty, to_uom_id=to_uom) + components[product]['qty'] += qty + else: + # To be in the uom reference of the product + to_uom = self.env['product.product'].browse(product).uom_id + if uom.id != to_uom.id: + from_uom = uom + qty = from_uom._compute_quantity(qty, to_uom_id=to_uom) + components[product] = {'qty': qty, 'uom': to_uom.id} + return components + class AccountInvoiceLine(models.Model): # TDE FIXME: what is this code ?? diff --git a/addons/sale_timesheet/models/procurement.py b/addons/sale_timesheet/models/procurement.py index bd7e4c85329..437a6f7398c 100644 --- a/addons/sale_timesheet/models/procurement.py +++ b/addons/sale_timesheet/models/procurement.py @@ -71,7 +71,7 @@ class ProcurementOrder(models.Model): 'partner_id': self.sale_line_id.order_id.partner_id.id or self.partner_dest_id.id, 'user_id': self.env.uid, 'procurement_id': self.id, - 'description': self.name + '\n', + 'description': self.name + '<br/>', 'project_id': project.id, 'company_id': self.company_id.id, } diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 7ce3bb2abed..902e0517ef5 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -273,6 +273,11 @@ class ProcurementOrder(models.Model): def _procurement_from_orderpoint_post_process(self, orderpoint_ids): return True + def _get_orderpoint_domain(self, company_id=False): + domain = [('company_id', '=', company_id)] if company_id else [] + domain += [('product_id.active', '=', True)] + return domain + @api.model def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False): """ Create procurements based on orderpoints. @@ -282,9 +287,7 @@ class ProcurementOrder(models.Model): """ OrderPoint = self.env['stock.warehouse.orderpoint'] - - domain = [('company_id', '=', company_id)] if company_id else [] - domain += [('product_id.active', '=', True)] + domain = self._get_orderpoint_domain(company_id=company_id) orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain, order=self._procurement_from_orderpoint_get_order()).ids while orderpoints_noprefetch: diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py index c1fd7aff521..6860ce7496b 100644 --- a/addons/stock/models/res_company.py +++ b/addons/stock/models/res_company.py @@ -23,14 +23,17 @@ class Company(models.Model): location = self.env['stock.location'].create({ 'name': _('%s: Transit Location') % self.name, 'usage': 'transit', - 'company_id': self.id, 'location_id': parent_location and parent_location.id or False, }) + location.sudo().write({'company_id': self.id}) self.write({'internal_transit_location_id': location.id}) @api.model def create(self, vals): company = super(Company, self).create(vals) - self.env['stock.warehouse'].create({'name': company.name, 'code': company.name[:5], 'company_id': company.id}) + + # multi-company rules prevents creating warehouse and sub-locations + self.env['stock.warehouse'].check_access_rights('create') + self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id}) company.create_transit_location() return company diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 591842e4225..b7ec53666ae 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -495,9 +495,12 @@ class StockMove(models.Model): self._push_apply() return self + def _set_default_price_moves(self): + return self.filtered(lambda move: not move.price_unit) + def set_default_price_unit_from_product(self): """ Set price to move, important in inter-company moves or receipts with only one partner """ - for move in self.filtered(lambda move: not move.price_unit): + for move in self._set_default_price_moves(): move.write({'price_unit': move.product_id.standard_price}) attribute_price = set_default_price_unit_from_product diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 5d5f2b2b4e6..ff42bf3095a 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -226,7 +226,7 @@ <h1 class="hidden-xs"> <field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/> </h1> - <group class="hidden-xs"> + <group> <group> <field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/> <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/> diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index fa373b6c9eb..ca8e2e840dc 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -179,6 +179,12 @@ class StockMove(models.Model): to_refund = fields.Boolean(string="To Refund (update SO/PO)", help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order') + def _set_default_price_moves(self): + # When the cost method is in real or average price, the price can be set to 0.0 on the PO + # So the price doesn't have to be updated + moves = super(StockMove, self)._set_default_price_moves() + return moves.filtered(lambda m: m.product_id.cost_method not in ('real', 'average')) + @api.multi def action_done(self): self.product_price_update_before_done() diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js index cdb3236aa64..badff89c846 100644 --- a/addons/web/static/src/js/widgets/debug_manager.js +++ b/addons/web/static/src/js/widgets/debug_manager.js @@ -168,6 +168,7 @@ var DebugManager = Widget.extend({ res_model: 'ir.ui.view', title: _t('Select a view'), disable_multiple_selection: true, + domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']], on_selected: function (element_ids) { self._rpc({ model: 'ir.ui.view', diff --git a/addons/web_tour/static/src/less/tip.less b/addons/web_tour/static/src/less/tip.less index c53ad943ff1..8f994e64ee7 100644 --- a/addons/web_tour/static/src/less/tip.less +++ b/addons/web_tour/static/src/less/tip.less @@ -1,6 +1,28 @@ .o_tooltip_parent { position: relative!important; + + // Tooltips are placed in the <body/> element when opened because this is + // the only way to position them above everything else thanks to a 1051 + // z-index. However, for scrolling performance, the tooltip is placed in its + // ideal location (see Tip._get_ideal_location). When in this location, the + // tooltip were sometimes overlapping unwanted elements (e.g. chat windows). + // + // The z-index of the ideal locations cannot be determined safely as they + // are different from one tip to another. So, in the appswitcher for + // example, the tooltips will act as root-level z-index 1051. As the chat + // windows are root-level z-index 3, the tooltips will overlap in that case. + // + // Changing the opacity of the tooltip parents forces the creation of a + // stacking context; the appswitcher tooltips are thus now considered to be + // root-level z-index auto (or the default appswitcher one) and should so + // act like their parent (e.g. the appswitcher is below the chat windows so + // the inner tooltips will be too). The tips will be above all elements of + // the appswitcher as they still have a high z-index, but relative to the + // appswitcher (this is especially useful in the website where most tooltips + // are placed in the body and need to be placed above elements with z-index + // like the navbar). + opacity: 0.999; } .o_tooltip { @@ -39,7 +61,7 @@ border: @o-tip-border-width solid @o-tip-color; border-radius: 50%; padding: 0; // ! Cannot be changed ! Javascript rely on this. - z-index: @zindex-modal+1; + z-index: @zindex-modal + 1; // See comment on 'o_tooltip_parent' class background-color: @o-tip-color; box-shadow: 0 2px 2px 1px rgba(150, 150, 150, 0.3); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index e4dd19e03db..ed4b810179c 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -520,52 +520,71 @@ animation.registry.gallery = animation.Class.extend({ animation.registry.gallery_slider = animation.Class.extend({ selector: ".o_slideshow", - start: function () { - var $carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel"); - var $indicator = $carousel.find('.carousel-indicators'); - var $lis = $indicator.find('li:not(.fa)'); - var $prev = $indicator.find('li.fa:first'); - var $next = $indicator.find('li.fa:last'); - var index = ($lis.filter('.active').index() || 1) -1; - var page = Math.floor(index / 10); - var nb = Math.ceil($lis.length / 10); - - // fix bootstrap use index insead of data-slide-to - $carousel.on('slide.bs.carousel', function () { + start: function (editable_mode) { + var self = this; + this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel"); + this.$indicator = this.$carousel.find('.carousel-indicators'); + this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden + this.$next = this.$indicator.find('li.fa:last').css('visibility', ''); + var $lis = this.$indicator.find('li:not(.fa)'); + var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space + var realNbPerPage = nbPerPage || 1; + var nbPages = Math.ceil($lis.length / realNbPerPage); + + var index; + var page; + update(); + + function hide() { + $lis.each(function (i) { + $(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage)); + }); + if (editable_mode) { // do not remove DOM in edit mode + return; + } + if (page <= 0) { + self.$prev.detach(); + } else { + self.$prev.prependTo(self.$indicator); + } + if (page >= nbPages - 1) { + self.$next.detach(); + } else { + self.$next.appendTo(self.$indicator); + } + } + + function update() { + index = $lis.index($lis.filter('.active')) || 0; + page = Math.floor(index / realNbPerPage); + hide(); + } + + this.$carousel.on('slide.bs.carousel.gallery_slider', function () { setTimeout(function () { - var $item = $carousel.find('.carousel-inner .prev, .carousel-inner .next'); + var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next'); var index = $item.index(); $lis.removeClass("active") .filter('[data-slide-to="'+index+'"]') .addClass("active"); - },0); - }); - - function hide () { - $lis.addClass('hidden').each(function (i) { - if (i >= page*10 && i < (page+1)*10) { - $(this).removeClass('hidden'); - } - }); - $prev.css('visibility', page === 0 ? 'hidden' : ''); - $next.css('visibility', (page+1) >= nb ? 'hidden' : ''); - } - - $indicator.find('li.fa').on('click', function () { - page = (page + ($(this).hasClass('o_indicators_left')?-1:1)) % nb; - $carousel.carousel(page*10); - hide(); + }, 0); }); - hide(); - - $carousel.on('slid.bs.carousel', function () { - var index = ($lis.filter('.active').index() || 1) -1; - page = Math.floor(index / 10); + this.$indicator.on('click.gallery_slider', '> li.fa', function () { + page += ($(this).hasClass('o_indicators_left') ? -1 : 1); + page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary + self.$carousel.carousel(page * realNbPerPage); hide(); }); + this.$carousel.on('slid.bs.carousel.gallery_slider', update); + }, + stop: function () { + this._super.apply(this, arguments); - return this._super.apply(this, arguments); - } + this.$prev.prependTo(this.$indicator); + this.$next.appendTo(this.$indicator); + this.$carousel.off('.gallery_slider'); + this.$indicator.off('.gallery_slider'); + }, }); }); diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index 40be63e3b9d..083ed195905 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -223,9 +223,6 @@ options.registry.carousel = options.registry.slider.extend({ // set background and prepare to clean for save this.$target.on('slid.bs.carousel', function () { - self.editor.styles.background_position.$target = self.editor.styles.background.$target; - self.editor.styles.background_position.set_active(); - self.editor.styles.background.$target.trigger("snippet-option-change", [self.editor.styles.background]); self.$target.carousel("pause"); if (!self.editor) return; diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less index 4fce9f14246..5f96bbac0e6 100644 --- a/addons/website/static/src/less/website.snippets.less +++ b/addons/website/static/src/less/website.snippets.less @@ -324,9 +324,6 @@ div.carousel .container { max-height: 100%; max-width: 100%; margin: auto; - position: relative; - top: 50%; - .translate(0, -50%); } ul.carousel-indicators { @@ -357,6 +354,10 @@ div.carousel .container { filter: gray; filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale"); } + + &:first-child { + margin-left: 0; + } } } diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index 640eb9a3121..8fab469c5c8 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -57,7 +57,14 @@ class Event(models.Model): if self.event_type_id: self.website_menu = self.event_type_id.website_menu + def _get_standard_menu_entries_names(self): + """ Method returning menu entries possibly generated by event modules. + It is used to distinguish module-generated pages from custom pages. """ + return [_('Introduction'), _('Location'), _('Register')] + def _get_menu_entries(self): + """ Method returning menu entries to display on the website view of the + event, possibly depending on some options in inheriting modules. """ self.ensure_one() return [ (_('Introduction'), False, 'website_event.template_intro'), @@ -75,9 +82,17 @@ class Event(models.Model): root_menu = self.env['website.menu'].create({'name': event.name}) event.menu_id = root_menu + existing_page_names = event.menu_id.child_id.mapped('name') + required_page_names = [entry[0] for entry in self._get_menu_entries()] + standard_page_names = self._get_standard_menu_entries_names() + + # remove entries that should not exist anymore + submenu_to_delete = event.menu_id.child_id.filtered(lambda menu: menu.name not in required_page_names and menu.name in standard_page_names) + submenu_to_delete.unlink() + + # create missing entries for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()): - existing_pages = event.menu_id.child_id.mapped('name') - if name not in existing_pages: + if name not in existing_page_names: if not url: newpath = self.env['website'].new_page(name + ' ' + self.name, xml_id, ispage=False) url = "/event/" + slug(self) + "/page/" + newpath diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py index 3acd8e8279f..baae681bc7b 100644 --- a/addons/website_event_track/models/event.py +++ b/addons/website_event_track/models/event.py @@ -80,6 +80,21 @@ class Event(models.Model): self.website_track = False self.website_track_proposal = False + @api.onchange('website_track') + def _onchange_website_track(self): + if not self.website_track: + self.website_track_proposal = False + + @api.onchange('website_track_proposal') + def _onchange_website_track_proposal(self): + if self.website_track_proposal: + self.website_track = True + + def _get_standard_menu_entries_names(self): + res = super(Event, self)._get_standard_menu_entries_names() + res += [_('Talks'), _('Agenda'), _('Talk Proposals')] + return res + def _get_menu_entries(self): self.ensure_one() res = super(Event, self)._get_menu_entries() diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py index 6a93f387ec1..11abd44ef76 100644 --- a/addons/website_form/controllers/main.py +++ b/addons/website_form/controllers/main.py @@ -171,7 +171,7 @@ class WebsiteForm(http.Controller): return data def insert_record(self, request, model, values, custom, meta=None): - record = request.env[model.model].sudo().create(values) + record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values) if custom or meta: default_field = model.website_form_default_field_id diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py index 9a64504e309..c9b3deb268e 100644 --- a/addons/website_mail_channel/controllers/main.py +++ b/addons/website_mail_channel/controllers/main.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import werkzeug + from datetime import datetime from dateutil import relativedelta @@ -119,6 +121,9 @@ class MailGroup(http.Controller): "/groups/<model('mail.channel'):group>/page/<int:page>" ], type='http', auth="public", website=True) def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post): + if group.channel_type != 'channel': + raise werkzeug.exceptions.NotFound() + Message = request.env['mail.message'] domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('message_type', '!=', 'notification')] @@ -151,6 +156,9 @@ class MailGroup(http.Controller): '''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>''', ], type='http', auth="public", website=True) def thread_discussion(self, group, message, mode='thread', date_begin=None, date_end=None, **post): + if group.channel_type != 'channel': + raise werkzeug.exceptions.NotFound() + Message = request.env['mail.message'] if mode == 'thread': base_domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('parent_id', '=', message.parent_id and message.parent_id.id or False)] @@ -175,6 +183,9 @@ class MailGroup(http.Controller): '''/groups/<model('mail.channel'):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group[0])]"):message>/get_replies''', type='json', auth="public", methods=['POST'], website=True) def render_messages(self, group, message, **post): + if group.channel_type != 'channel': + return False + last_displayed_id = post.get('last_displayed_id') if not last_displayed_id: return False diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 63bf22b2a8b..917fb4258da 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -465,7 +465,7 @@ <a id="add_to_cart" class="btn btn-primary btn-lg mt8 js_check_product a-submit" href="#">Add to Cart</a> </div> </form> - <hr /> + <hr t-if="product.availability != 'empty'"/> <div t-if="product.availability == 'in_stock'" class="alert alert-success"> <span class="fa fa-check" /> <span>In Stock</span> @@ -1431,7 +1431,7 @@ <div class="col-sm-12"> <form target="_self" action="/shop/payment/validate" method="post" class="pull-right"> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" /> - <a style="width:127px;" class="btn btn-primary a-submit"> + <a class="btn btn-primary a-submit"> <span t-if="order.amount_total > 0">Pay Now <span class="fa fa-long-arrow-right"></span></span> <span t-if="order.amount_total == 0">Confirm Order <span class="fa fa-long-arrow-right"></span></span> </a> |
